Reimplement EAS contacts sync to work w/ new system facilities

* Modify to work with ContactsProvider2
* Modify to work with system AccountManager
* Modify to work with system SyncManager (for triggering user-change syncs)
* Sync server->client for adds/deletes implemented (CP2 doesn't handle delete yet)
* Sync server->client changes handled efficiently (only write changes)
* Some fields still not handled
* Rewrote most of the CPO code to handle server->client changes
* Sync client->server works for supported fields
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index aa2929b..44504b4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -24,6 +24,7 @@
     <uses-permission android:name="android.permission.VIBRATE"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
  
     <!--  For EAS purposes; could be removed when EAS has a permanent home -->
     <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
@@ -174,6 +175,17 @@
             >
         </service>
         
+        <!--Required stanza to register the ContactsSyncAdapterService with SyncManager -->
+        <service 
+            android:name="com.android.exchange.ContactsSyncAdapterService" 
+        	android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                       android:resource="@xml/syncadapter_contacts" />
+        </service>
+
         <!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
         <service
             android:name="com.android.exchange.SyncManager"
diff --git a/res/xml/syncadapter_contacts.xml b/res/xml/syncadapter_contacts.xml
new file mode 100644
index 0000000..4691aee
--- /dev/null
+++ b/res/xml/syncadapter_contacts.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, 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.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="com.android.contacts"
+    android:accountType="com.android.exchange"
+/>
diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
new file mode 100644
index 0000000..2c38b9d
--- /dev/null
+++ b/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2009 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;
+
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+
+import android.accounts.Account;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+public class ContactsSyncAdapterService extends Service {
+    private final String TAG = "EAS ContactsSyncAdapterService";
+    private final SyncAdapterImpl mSyncAdapter;
+
+    private static final String[] ID_PROJECTION = new String[] {EmailContent.RECORD_ID};
+    private static final String ACCOUNT_AND_TYPE_CONTACTS =
+        MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CONTACTS;
+
+    public ContactsSyncAdapterService() {
+        super();
+        mSyncAdapter = new SyncAdapterImpl();
+    }
+
+    private class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
+        public SyncAdapterImpl() {
+            super(ContactsSyncAdapterService.this);
+        }
+
+        @Override
+        public void performSync(Account account, Bundle extras,
+                String authority, ContentProviderClient provider, SyncResult syncResult) {
+            try {
+                ContactsSyncAdapterService.this.performSync(account, extras,
+                        authority, provider, syncResult);
+            } catch (OperationCanceledException e) {
+            }
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mSyncAdapter.getISyncAdapter().asBinder();
+    }
+
+    /**
+     * Partial integration with system SyncManager; we tell our EAS SyncManager to start a contacts
+     * sync when we get the signal from the system SyncManager.
+     * The missing piece at this point is integration with the push/ping mechanism in EAS; this will
+     * be put in place at a later time.
+     */
+    private void performSync(Account account, Bundle extras, String authority,
+            ContentProviderClient provider, SyncResult syncResult)
+            throws OperationCanceledException {
+        ContentResolver cr = getContentResolver();
+        // Find the (EmailProvider) account associated with this email address
+        Cursor accountCursor =
+            cr.query(com.android.email.provider.EmailContent.Account.CONTENT_URI, ID_PROJECTION,
+                AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.mName}, null);
+        try {
+            if (accountCursor.moveToFirst()) {
+                long accountId = accountCursor.getLong(0);
+                // Now, find the contacts mailbox associated with the account
+                Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
+                        ACCOUNT_AND_TYPE_CONTACTS, new String[] {Long.toString(accountId)}, null);
+                try {
+                    if (mailboxCursor.moveToFirst()) {
+                        Log.i(TAG, "Contact sync requested for " + account.mName);
+                        // Ask for a sync from our sync manager
+                        SyncManager.serviceRequest(mailboxCursor.getLong(0));
+                    }
+                } finally {
+                    mailboxCursor.close();
+                }
+            }
+        } finally {
+            accountCursor.close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index d8f46ab..0472aa9 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -347,6 +347,7 @@
         return setupEASCommand(method, cmd, null);
     }
 
+    @SuppressWarnings("deprecation")
     private String makeUriString(String cmd, String extra) {
          // Cache the authentication string and the command string
         if (mDeviceId == null)
@@ -691,7 +692,7 @@
             BufferedReader rdr = null;
             String id;
             if (f.exists() && f.canRead()) {
-                rdr = new BufferedReader(new FileReader(f));
+                rdr = new BufferedReader(new FileReader(f), 128);
                 id = rdr.readLine();
                 rdr.close();
                 return id;
@@ -853,7 +854,9 @@
         mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
         mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
         try {
-            if (mMailbox.mServerId.equals(Eas.ACCOUNT_MAILBOX)) {
+            if (mMailbox == null || mAccount == null) {
+                return;
+            } else if (mMailbox.mServerId.equals(Eas.ACCOUNT_MAILBOX)) {
                 runMain();
             } else {
                 EasSyncAdapter target;
@@ -861,9 +864,9 @@
                 mProtocolVersion = mAccount.mProtocolVersion;
                 mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
                 if (mMailbox.mType == Mailbox.TYPE_CONTACTS)
-                    target = new EasContactsSyncAdapter(mMailbox);
+                    target = new EasContactsSyncAdapter(mMailbox, this);
                 else {
-                    target = new EasEmailSyncAdapter(mMailbox);
+                    target = new EasEmailSyncAdapter(mMailbox, this);
                 }
                 // We loop here because someone might have put a request in while we were syncing
                 // and we've missed that opportunity...
diff --git a/src/com/android/exchange/UserSyncAlarmReceiver.java b/src/com/android/exchange/EmailSyncAlarmReceiver.java
similarity index 89%
rename from src/com/android/exchange/UserSyncAlarmReceiver.java
rename to src/com/android/exchange/EmailSyncAlarmReceiver.java
index a027aa0..bea5328 100644
--- a/src/com/android/exchange/UserSyncAlarmReceiver.java
+++ b/src/com/android/exchange/EmailSyncAlarmReceiver.java
@@ -31,7 +31,7 @@
 import java.util.ArrayList;
 
 /**
- * UserSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
+ * EmailSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
  * back to the Exchange server.
  *
  * Here's how this works for Email, for example:
@@ -40,15 +40,15 @@
  * 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change
  * 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the
  * future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism).
- * 4) USAR Receiver's onReceive method is called
- * 5) USAR goes through all change and deletion records and compiles a list of mailboxes which have
+ * 4) ESAR Receiver's onReceive method is called
+ * 5) ESAR goes through all change and deletion records and compiles a list of mailboxes which have
  * changes to be uploaded.
- * 6) USAR calls SyncManager to start syncs of those mailboxes
+ * 6) ESAR calls SyncManager to start syncs of those mailboxes
  *
  */
-public class UserSyncAlarmReceiver extends BroadcastReceiver {
+public class EmailSyncAlarmReceiver extends BroadcastReceiver {
     final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY, SyncColumns.DATA};
-    private static String TAG = "UserSyncAlarm";
+    private static String TAG = "EmailSyncAlarm";
 
     @Override
     public void onReceive(Context context, Intent intent) {
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 07a9489..2be090b 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -59,7 +59,7 @@
 
 /**
  * The SyncManager handles all aspects of starting, maintaining, and stopping the various sync
- * adapters used by Exchange.  However, it is capable of handing any kind of email sync, and it 
+ * adapters used by Exchange.  However, it is capable of handing any kind of email sync, and it
  * would be appropriate to use for IMAP push, when that functionality is added to the Email
  * application.
  *
@@ -91,17 +91,17 @@
     MessageObserver mMessageObserver;
     String mNextWaitReason;
     IEmailServiceCallback mCallback;
-    
+
     RemoteCallbackList<IEmailServiceCallback> mCallbackList =
         new RemoteCallbackList<IEmailServiceCallback>();
-    
+
     static private HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
     static private HashMap<Long, PendingIntent> mPendingIntents =
         new HashMap<Long, PendingIntent>();
     static private WakeLock mWakeLock = null;
 
     /**
-     * Create the binder for EmailService implementation here.  These are the calls that are 
+     * Create the binder for EmailService implementation here.  These are the calls that are
      * defined in AbstractSyncService.   Only validate is now implemented; loadAttachment currently
      * spins its wheels counting up to 100%.
      */
@@ -183,24 +183,24 @@
         }
     };
 
+    class AccountList extends ArrayList<Account> {
+        private static final long serialVersionUID = 1L;
+
+        public boolean contains(long id) {
+            for (Account account: this) {
+                if (account.mId == id) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
     class AccountObserver extends ContentObserver {
 
         // mAccounts keeps track of Accounts that we care about (EAS for now)
         AccountList mAccounts = new AccountList();
 
-        class AccountList extends ArrayList<Account> {
-            private static final long serialVersionUID = 1L;
-
-            public boolean contains(long id) {
-                for (Account account: this) {
-                    if (account.mId == id) {
-                        return true;
-                    }
-                }
-                return false;
-            }
-        }
-
         public AccountObserver(Handler handler) {
             super(handler);
             Context context = getContext();
@@ -237,6 +237,7 @@
             return false;
         }
 
+        @Override
         public void onChange(boolean selfChange) {
             // A change to the list requires us to scan for deletions (to stop running syncs)
             // At startup, we want to see what accounts exist and cache them
@@ -328,6 +329,7 @@
             super(handler);
         }
 
+        @Override
         public void onChange(boolean selfChange) {
             // See if there's anything to do...
             kick();
@@ -337,7 +339,7 @@
     class SyncedMessageObserver extends ContentObserver {
         long maxChangedId = 0;
         long maxDeletedId = 0;
-        Intent syncAlarmIntent = new Intent(INSTANCE, UserSyncAlarmReceiver.class);
+        Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
         PendingIntent syncAlarmPendingIntent =
             PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
         AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
@@ -347,6 +349,7 @@
             super(handler);
         }
 
+        @Override
         public void onChange(boolean selfChange) {
             INSTANCE.log("SyncedMessage changed: (re)setting alarm for 10s");
             alarmManager.set(AlarmManager.RTC_WAKEUP,
@@ -360,6 +363,7 @@
             super(handler);
         }
 
+        @Override
         public void onChange(boolean selfChange) {
             INSTANCE.log("MessageObserver");
             // A rather blunt instrument here.  But we don't have information about the URI that
@@ -374,7 +378,15 @@
         }
         return null;
     }
-    
+
+    static public AccountList getAccountList() {
+        if (INSTANCE != null) {
+            return INSTANCE.mAccountObserver.mAccounts;
+        } else {
+            return null;
+        }
+    }
+
     public class SyncStatus {
         static public final int NOT_RUNNING = 0;
         static public final int DIED = 1;
@@ -651,13 +663,6 @@
     public void run() {
         mStop = false;
 
-//        if (Debug.isDebuggerConnected()) {
-//            try {
-//                Thread.sleep(10000L);
-//            } catch (InterruptedException e) {
-//            }
-//        }
-
         runAwake(-1);
 
         ContentResolver resolver = getContentResolver();
@@ -668,7 +673,7 @@
 
         ConnectivityReceiver cr = new ConnectivityReceiver();
         registerReceiver(cr, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
-        ConnectivityManager cm = 
+        ConnectivityManager cm =
             (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
 
         try {
@@ -721,6 +726,22 @@
     }
 
     long checkMailboxes () {
+        // First, see if any running mailboxes have been deleted
+        ArrayList<Long> deadMailboxes = new ArrayList<Long>();
+        synchronized (mSyncToken) {
+            for (long mailboxId: mServiceMap.keySet()) {
+                Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+                if (m == null) {
+                    deadMailboxes.add(mailboxId);
+                    log("Stopping sync for mailbox " + mailboxId + "; record not found.");
+                }
+            }
+        }
+        // If so, stop them
+        for (Long mailboxId: deadMailboxes) {
+            stopManualSync(mailboxId);
+        }
+
         long nextWait = 10*MINS;
         long now = System.currentTimeMillis();
         // Start up threads that need it...
@@ -800,7 +821,7 @@
         }
         return nextWait;
     }
-        
+
     static public void serviceRequest(Mailbox m) {
         serviceRequest(m.mId, 5*SECS);
     }
@@ -881,7 +902,7 @@
     /**
      * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in
      * an error state
-     * 
+     *
      * @param mailboxId
      * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
      */
@@ -896,7 +917,7 @@
         }
         return true;
     }
-    
+
     static public int getSyncStatus(long mailboxId) {
         synchronized (mSyncToken) {
             if (INSTANCE == null || INSTANCE.mServiceMap == null) {
@@ -984,30 +1005,32 @@
      * @param svc the service that is finished
      */
     static public void done(AbstractSyncService svc) {
-        long mailboxId = svc.mMailboxId;
-        HashMap<Long, SyncError> errorMap = INSTANCE.mSyncErrorMap;
-        SyncError syncError = errorMap.get(mailboxId);
-        INSTANCE.mServiceMap.remove(mailboxId);
-        int exitStatus = svc.mExitStatus;
-        switch (exitStatus) {
-            case AbstractSyncService.EXIT_DONE:
-                if (!svc.mPartRequests.isEmpty()) {
-                    // TODO Handle this case
-                }
-                errorMap.remove(mailboxId);
-                break;
-            case AbstractSyncService.EXIT_IO_ERROR:
-                if (syncError != null) {
-                    syncError.escalate();
-                } else {
-                    errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, false));
-                }
-                kick();
-                break;
-            case AbstractSyncService.EXIT_LOGIN_FAILURE:
-            case AbstractSyncService.EXIT_EXCEPTION:
-                errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, true));
-                break;
+        synchronized(mSyncToken) {
+            long mailboxId = svc.mMailboxId;
+            HashMap<Long, SyncError> errorMap = INSTANCE.mSyncErrorMap;
+            SyncError syncError = errorMap.get(mailboxId);
+            INSTANCE.mServiceMap.remove(mailboxId);
+            int exitStatus = svc.mExitStatus;
+            switch (exitStatus) {
+                case AbstractSyncService.EXIT_DONE:
+                    if (!svc.mPartRequests.isEmpty()) {
+                        // TODO Handle this case
+                    }
+                    errorMap.remove(mailboxId);
+                    break;
+                case AbstractSyncService.EXIT_IO_ERROR:
+                    if (syncError != null) {
+                        syncError.escalate();
+                    } else {
+                        errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, false));
+                    }
+                    kick();
+                    break;
+                case AbstractSyncService.EXIT_LOGIN_FAILURE:
+                case AbstractSyncService.EXIT_EXCEPTION:
+                    errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, true));
+                    break;
+            }
         }
     }
 
@@ -1034,6 +1057,6 @@
         if (INSTANCE == null) {
             return null;
         }
-        return (Context)INSTANCE;
+        return INSTANCE;
     }
 }
diff --git a/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java b/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java
index d0b1c52..ccbb1d4 100644
--- a/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EasCalendarSyncAdapter.java
@@ -29,8 +29,8 @@
  */
 public class EasCalendarSyncAdapter extends EasSyncAdapter {
 
-    public EasCalendarSyncAdapter(Mailbox mailbox) {
-        super(mailbox);
+    public EasCalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        super(mailbox, service);
     }
 
     @Override
diff --git a/src/com/android/exchange/adapter/EasContactsSyncAdapter.java b/src/com/android/exchange/adapter/EasContactsSyncAdapter.java
index 5cd0855..a4ec93e 100644
--- a/src/com/android/exchange/adapter/EasContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EasContactsSyncAdapter.java
@@ -18,15 +18,34 @@
 package com.android.exchange.adapter;
 
 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.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.OperationApplicationException;
+import android.content.ContentProviderOperation.Builder;
+import android.content.Entity.NamedContentValues;
 import android.database.Cursor;
 import android.net.Uri;
-import android.provider.Contacts;
-import android.provider.Contacts.People;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -39,14 +58,32 @@
  */
 public class EasContactsSyncAdapter extends EasSyncAdapter {
 
-    private static final String WHERE_SERVER_ID_AND_ACCOUNT = "_sync_id=?";
+    private static final String TAG = "EasContactsSyncAdapter";
+    private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
+    private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
+
+    // Note: These constants are likely to change; they are internal to this class now, but
+    // may end up in the provider.
+    private static final int TYPE_EMAIL1 = 20;
+    private static final int TYPE_EMAIL2 = 21;
+    private static final int TYPE_EMAIL3 = 22;
+
+    private static final int TYPE_IM1 = 23;
+    private static final int TYPE_IM2 = 24;
+    private static final int TYPE_IM3 = 25;
+
+    private static final int TYPE_WORK2 = 26;
+    private static final int TYPE_HOME2 = 27;
+    private static final int TYPE_CAR = 28;
+    private static final int TYPE_COMPANY_MAIN = 29;
+    private static final int TYPE_MMS = 30;
+    private static final int TYPE_RADIO = 31;
 
     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
-
     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
 
-    public EasContactsSyncAdapter(Mailbox mailbox) {
-        super(mailbox);
+    public EasContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        super(mailbox, service);
     }
 
     @Override
@@ -55,57 +92,74 @@
         return p.parse();
     }
 
+    public static final class Extras {
+        private Extras() {}
+
+        /** MIME type used when storing this in data table. */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/easextras";
+
+        /**
+         * The note text.
+         * <P>Type: TEXT</P>
+         */
+        public static final String EXTRAS = "data2";
+    }
+
     class EasContactsSyncParser extends EasContentParser {
 
         String[] mBindArgument = new String[1];
-
         String mMailboxIdAsString;
-
-        StringBuilder mExtraData = new StringBuilder(1024);
+        Uri mAccountUri;
 
         public EasContactsSyncParser(InputStream in, EasSyncService service) throws IOException {
             super(in, service);
-            //setDebug(true); // DON'T CHECK IN WITH THIS UNCOMMENTED
-        }
-
-        class ContactMethod {
-            ContentValues values = new ContentValues();
-
-            ContactMethod(int kind, int type, String value) {
-                values.put(Contacts.ContactMethods.KIND, kind);
-                values.put(Contacts.ContactMethods.TYPE, type);
-                values.put(Contacts.ContactMethods.DATA, value);
-            }
-        }
-
-        class Phone {
-            ContentValues values = new ContentValues();
-
-            Phone(int type, String value) {
-                values.put(Contacts.Phones.TYPE, type);
-                values.put(Contacts.Phones.NUMBER, value);
-            }
+            mAccountUri = uriWithAccount(RawContacts.CONTENT_URI);
+            setDebug(false); // DON'T CHECK IN WITH THIS UNCOMMENTED
         }
 
         @Override
         public void wipe() {
-            // TODO Auto-generated method stub
+            // TODO Uncomment when the new provider works with this
+            //mContentResolver.delete(mAccountUri, null, null);
         }
 
-        void saveExtraData (int tag) throws IOException {
-            mExtraData.append(name);
-            mExtraData.append("~");
-            mExtraData.append(getValue());
-            mExtraData.append('~');
+        void saveExtraData (StringBuilder extras, int tag) throws IOException {
+            // TODO Handle containers (categories/children)
+            extras.append(tag);
+            extras.append("~");
+            extras.append(getValue());
+            extras.append('~');
         }
 
-        public void addData(String serverId, ArrayList<ContentProviderOperation> ops) 
+        class Address {
+            String city;
+            String country;
+            String code;
+            String street;
+            String state;
+
+            boolean hasData() {
+                return city != null || country != null || code != null || state != null
+                    || street != null;
+            }
+        }
+
+        public void addData(String serverId, ContactOperations ops, Entity entity)
                 throws IOException {
             String firstName = null;
             String lastName = null;
             String companyName = null;
-            ArrayList<ContactMethod> contactMethods = new ArrayList<ContactMethod>();
-            ArrayList<Phone> phones = new ArrayList<Phone>();
+            String title = null;
+            Address home = new Address();
+            Address work = new Address();
+            Address other = new Address();
+
+            if (entity == null) {
+                ops.newContact(serverId);
+            }
+
+            StringBuilder extraData = new StringBuilder(1024);
+
             while (nextTag(EasTags.SYNC_APPLICATION_DATA) != END) {
                 switch (tag) {
                     case EasTags.CONTACTS_FIRST_NAME:
@@ -117,33 +171,122 @@
                     case EasTags.CONTACTS_COMPANY_NAME:
                         companyName = getValue();
                         break;
+                    case EasTags.CONTACTS_JOB_TITLE:
+                        title = getValue();
+                        break;
                     case EasTags.CONTACTS_EMAIL1_ADDRESS:
+                        ops.addEmail(entity, TYPE_EMAIL1, getValue());
+                        break;
                     case EasTags.CONTACTS_EMAIL2_ADDRESS:
+                        ops.addEmail(entity, TYPE_EMAIL2, getValue());
+                        break;
                     case EasTags.CONTACTS_EMAIL3_ADDRESS:
-                        contactMethods.add(new ContactMethod(Contacts.KIND_EMAIL, 
-                                Contacts.ContactMethods.TYPE_OTHER, getValue()));
+                        ops.addEmail(entity, TYPE_EMAIL3, getValue());
                         break;
                     case EasTags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
+                        ops.addPhone(entity, TYPE_WORK2, getValue());
+                        break;
                     case EasTags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
-                        phones.add(new Phone(Contacts.Phones.TYPE_WORK, getValue()));
+                        ops.addPhone(entity, Phone.TYPE_WORK, getValue());
+                        break;
+                    case EasTags.CONTACTS2_MMS:
+                        ops.addPhone(entity, TYPE_MMS, getValue());
                         break;
                     case EasTags.CONTACTS_BUSINESS_FAX_NUMBER:
-                        phones.add(new Phone(Contacts.Phones.TYPE_FAX_WORK, getValue()));
+                        ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
+                        break;
+                    case EasTags.CONTACTS2_COMPANY_MAIN_PHONE:
+                        ops.addPhone(entity, TYPE_COMPANY_MAIN, getValue());
                         break;
                     case EasTags.CONTACTS_HOME_FAX_NUMBER:
-                        phones.add(new Phone(Contacts.Phones.TYPE_FAX_HOME, getValue()));
+                        ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
                         break;
                     case EasTags.CONTACTS_HOME_TELEPHONE_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_HOME, getValue());
+                        break;
                     case EasTags.CONTACTS_HOME2_TELEPHONE_NUMBER:
-                        phones.add(new Phone(Contacts.Phones.TYPE_HOME, getValue()));
+                        ops.addPhone(entity, TYPE_HOME2, getValue());
                         break;
                     case EasTags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
+                        ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
+                        break;
                     case EasTags.CONTACTS_CAR_TELEPHONE_NUMBER:
-                        phones.add(new Phone(Contacts.Phones.TYPE_MOBILE, getValue()));
+                        ops.addPhone(entity, TYPE_CAR, getValue());
+                        break;
+                    case EasTags.CONTACTS_RADIO_TELEPHONE_NUMBER:
+                        ops.addPhone(entity, TYPE_RADIO, getValue());
                         break;
                     case EasTags.CONTACTS_PAGER_NUMBER:
-                        phones.add(new Phone(Contacts.Phones.TYPE_PAGER, getValue()));
+                        ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
                         break;
+                    case EasTags.CONTACTS2_IM_ADDRESS:
+                        ops.addIm(entity, TYPE_IM1, getValue());
+                        break;
+                    case EasTags.CONTACTS2_IM_ADDRESS_2:
+                        ops.addIm(entity, TYPE_IM2, getValue());
+                        break;
+                    case EasTags.CONTACTS2_IM_ADDRESS_3:
+                        ops.addIm(entity, TYPE_IM3, getValue());
+                        break;
+                    case EasTags.CONTACTS_BUSINESS_ADDRESS_CITY:
+                        work.city = getValue();
+                        break;
+                    case EasTags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
+                        work.country = getValue();
+                        break;
+                    case EasTags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
+                        work.code = getValue();
+                        break;
+                    case EasTags.CONTACTS_BUSINESS_ADDRESS_STATE:
+                        work.state = getValue();
+                        break;
+                    case EasTags.CONTACTS_BUSINESS_ADDRESS_STREET:
+                        work.street = getValue();
+                        break;
+                    case EasTags.CONTACTS_HOME_ADDRESS_CITY:
+                        home.city = getValue();
+                        break;
+                    case EasTags.CONTACTS_HOME_ADDRESS_COUNTRY:
+                        home.country = getValue();
+                        break;
+                    case EasTags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
+                        home.code = getValue();
+                        break;
+                    case EasTags.CONTACTS_HOME_ADDRESS_STATE:
+                        home.state = getValue();
+                        break;
+                    case EasTags.CONTACTS_HOME_ADDRESS_STREET:
+                        home.street = getValue();
+                        break;
+                    case EasTags.CONTACTS_OTHER_ADDRESS_CITY:
+                        other.city = getValue();
+                        break;
+                    case EasTags.CONTACTS_OTHER_ADDRESS_COUNTRY:
+                        other.country = getValue();
+                        break;
+                    case EasTags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
+                        other.code = getValue();
+                        break;
+                    case EasTags.CONTACTS_OTHER_ADDRESS_STATE:
+                        other.state = getValue();
+                        break;
+                    case EasTags.CONTACTS_OTHER_ADDRESS_STREET:
+                        other.street = getValue();
+                        break;
+
+                    case EasTags.CONTACTS_CHILDREN:
+                        childrenParser(extraData);
+                        break;
+
+                    case EasTags.CONTACTS_CATEGORIES:
+                        categoriesParser(extraData);
+                        break;
+
+                        // TODO We'll add this later
+                    case EasTags.CONTACTS_PICTURE:
+                        getValue();
+                        break;
+
                     // All tags that we don't use (except for a few like picture and body) need to
                     // be saved, even if we're not using them.  Otherwise, when we upload changes,
                     // those items will be deleted back on the server.
@@ -151,64 +294,31 @@
                     case EasTags.CONTACTS_ASSISTANT_NAME:
                     case EasTags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
                     case EasTags.CONTACTS_BIRTHDAY:
-                    case EasTags.CONTACTS_BUSINESS_ADDRESS_CITY:
-                    case EasTags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
-                    case EasTags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
-                    case EasTags.CONTACTS_BUSINESS_ADDRESS_STATE:
-                    case EasTags.CONTACTS_BUSINESS_ADDRESS_STREET:
-                    case EasTags.CONTACTS_CATEGORIES:
-                    case EasTags.CONTACTS_CATEGORY:
-                    case EasTags.CONTACTS_CHILDREN:
-                    case EasTags.CONTACTS_CHILD:
                     case EasTags.CONTACTS_DEPARTMENT:
                     case EasTags.CONTACTS_FILE_AS:
-                    case EasTags.CONTACTS_HOME_ADDRESS_CITY:
-                    case EasTags.CONTACTS_HOME_ADDRESS_COUNTRY:
-                    case EasTags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
-                    case EasTags.CONTACTS_HOME_ADDRESS_STATE:
-                    case EasTags.CONTACTS_HOME_ADDRESS_STREET:
-                    case EasTags.CONTACTS_JOB_TITLE:
+                    case EasTags.CONTACTS_TITLE:
                     case EasTags.CONTACTS_MIDDLE_NAME:
                     case EasTags.CONTACTS_OFFICE_LOCATION:
-                    case EasTags.CONTACTS_OTHER_ADDRESS_CITY:
-                    case EasTags.CONTACTS_OTHER_ADDRESS_COUNTRY:
-                    case EasTags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
-                    case EasTags.CONTACTS_OTHER_ADDRESS_STATE:
-                    case EasTags.CONTACTS_OTHER_ADDRESS_STREET:
-                    case EasTags.CONTACTS_RADIO_TELEPHONE_NUMBER:
                     case EasTags.CONTACTS_SPOUSE:
                     case EasTags.CONTACTS_SUFFIX:
-                    case EasTags.CONTACTS_TITLE:
                     case EasTags.CONTACTS_WEBPAGE:
                     case EasTags.CONTACTS_YOMI_COMPANY_NAME:
                     case EasTags.CONTACTS_YOMI_FIRST_NAME:
                     case EasTags.CONTACTS_YOMI_LAST_NAME:
                     case EasTags.CONTACTS_COMPRESSED_RTF:
-                    //case EasTags.CONTACTS_PICTURE:
                     case EasTags.CONTACTS2_CUSTOMER_ID:
                     case EasTags.CONTACTS2_GOVERNMENT_ID:
-                    case EasTags.CONTACTS2_IM_ADDRESS:
-                    case EasTags.CONTACTS2_IM_ADDRESS_2:
-                    case EasTags.CONTACTS2_IM_ADDRESS_3:
                     case EasTags.CONTACTS2_MANAGER_NAME:
-                    case EasTags.CONTACTS2_COMPANY_MAIN_PHONE:
                     case EasTags.CONTACTS2_ACCOUNT_NAME:
                     case EasTags.CONTACTS2_NICKNAME:
-                    case EasTags.CONTACTS2_MMS:
-                        saveExtraData(tag);
+                        saveExtraData(extraData, tag);
                         break;
                     default:
                         skipTag();
                 }
             }
 
-            // Ok, ready to create our contact...
-            // First pass, no batch...  Eventually, move to changesParser
-            ContentValues values = new ContentValues();
-
-            // TODO Do something with the extras (i.e. find a home for them)
-            String extraData = mExtraData.toString();
-            mService.userLog(extraData);
+            ops.addExtras(entity, extraData.toString());
 
             // We must have first name, last name, or company name
             String name;
@@ -225,31 +335,49 @@
             } else {
                 return;
             }
+            ops.addName(entity, firstName, lastName, name);
 
-            values.put(Contacts.People.NAME, name);
-            values.put("_sync_id", serverId);
-            // TODO Use proper value here; need to ask jham
-            //values.put("_sync_account", "EAS");
-            Uri contactUri = 
-                Contacts.People.createPersonInMyContactsGroup(mContentResolver, values);
-
-            Uri contactMethodsUri = Uri.withAppendedPath(contactUri,
-                    Contacts.People.ContactMethods.CONTENT_DIRECTORY);
-            for (ContactMethod cm: contactMethods) {
-                mContentResolver.insert(contactMethodsUri, cm.values);
-                //ops.add(ContentProviderOperation
-                //        .newInsert(contactMethodsUri).withValues(cm.values).build());
+            if (work.hasData()) {
+                ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
+                        work.state, work.country, work.code);
+            }
+            if (home.hasData()) {
+                ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
+                        home.state, home.country, home.code);
+            }
+            if (other.hasData()) {
+                ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
+                        other.state, other.country, other.code);
             }
 
-            Uri phoneUri = Uri.withAppendedPath(contactUri, People.Phones.CONTENT_DIRECTORY);
-            for (Phone phone: phones) {
-                mContentResolver.insert(phoneUri, phone.values);
-                //ops.add(ContentProviderOperation
-                //        .newInsert(phoneUri).withValues(phone.values).build());
+            if (companyName != null) {
+                ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title);
             }
         }
 
-        public void addParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+        private void categoriesParser(StringBuilder extras) throws IOException {
+            while (nextTag(EasTags.CONTACTS_CATEGORIES) != END) {
+                switch (tag) {
+                    case EasTags.CONTACTS_CATEGORY:
+                        saveExtraData(extras, tag);
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        private void childrenParser(StringBuilder extras) throws IOException {
+            while (nextTag(EasTags.CONTACTS_CHILDREN) != END) {
+                switch (tag) {
+                    case EasTags.CONTACTS_CHILD:
+                        saveExtraData(extras, tag);
+                    default:
+                        skipTag();
+                }
+            }
+        }
+
+        public void addParser(ContactOperations ops) throws IOException {
             String serverId = null;
             while (nextTag(EasTags.SYNC_ADD) != END) {
                 switch (tag) {
@@ -257,7 +385,7 @@
                         serverId = getValue();
                         break;
                     case EasTags.SYNC_APPLICATION_DATA:
-                        addData(serverId, ops);
+                        addData(serverId, ops, null);
                         break;
                     default:
                         skipTag();
@@ -267,13 +395,11 @@
 
         private Cursor getServerIdCursor(String serverId) {
             mBindArgument[0] = serverId;
-            //bindArguments[1] = "EAS";
-            // TODO Find proper constant for _id
-            return mContentResolver.query(Contacts.People.CONTENT_URI, new String[] {"_id"},
-                    WHERE_SERVER_ID_AND_ACCOUNT, mBindArgument, null);
+            return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
+                    mBindArgument, null);
         }
 
-        public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+        public void deleteParser(ContactOperations ops) throws IOException {
             while (nextTag(EasTags.SYNC_DELETE) != END) {
                 switch (tag) {
                     case EasTags.SYNC_SERVER_ID:
@@ -283,12 +409,7 @@
                         try {
                             if (c.moveToFirst()) {
                                 mService.userLog("Deleting " + serverId);
-                                mContentResolver.delete(ContentUris
-                                        .withAppendedId(Contacts.People.CONTENT_URI, c.getLong(0)),
-                                        null, null);
-                                //ops.add(ContentProviderOperation.newDelete(
-                                //        ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 
-                                //                c.getLong(0))).build());
+                                ops.delete(c.getLong(0));
                             }
                         } finally {
                             c.close();
@@ -311,14 +432,13 @@
         }
 
         /**
-         * A change operation on a contact is implemented as a delete followed by an add, since the
-         * change data is always a full contact.
-         * 
+         * Changes are handled row by row, and only changed/new rows are acted upon
          * @param ops the array of pending ContactProviderOperations.
          * @throws IOException
          */
-        public void changeParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+        public void changeParser(ContactOperations ops) throws IOException {
             String serverId = null;
+            Entity entity = null;
             while (nextTag(EasTags.SYNC_CHANGE) != END) {
                 switch (tag) {
                     case EasTags.SYNC_SERVER_ID:
@@ -326,28 +446,35 @@
                         Cursor c = getServerIdCursor(serverId);
                         try {
                             if (c.moveToFirst()) {
-                                mContentResolver.delete(ContentUris
-                                        .withAppendedId(Contacts.People.CONTENT_URI, c.getLong(0)),
-                                        null, null);
-                                //ops.add(ContentProviderOperation.newDelete(
-                                //        ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 
-                                //                c.getLong(0))).build());
-                               mService.userLog("Changing " + serverId);
+                                // TODO Handle deleted individual rows...
+                                try {
+                                    EntityIterator entityIterator =
+                                        mContentResolver.queryEntities(ContentUris
+                                            .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)),
+                                            null, null, null);
+                                    if (entityIterator.hasNext()) {
+                                        entity = entityIterator.next();
+                                    }
+                                    mService.userLog("Changing contact " + serverId);
+                                } catch (RemoteException e) {
+                                }
                             }
                         } finally {
                             c.close();
                         }
                         break;
                     case EasTags.SYNC_APPLICATION_DATA:
-                        addData(serverId, ops);
+                        addData(serverId, ops, entity);
+                        break;
                     default:
                         skipTag();
                 }
             }
         }
 
+        @Override
         public void commandsParser() throws IOException {
-            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+            ContactOperations ops = new ContactOperations();
             while (nextTag(EasTags.SYNC_COMMANDS) != END) {
                 if (tag == EasTags.SYNC_ADD) {
                     addParser(ops);
@@ -359,22 +486,363 @@
                     skipTag();
             }
 
-            // Batch provider operations here
-//            try {
-//                mService.mContext.getContentResolver()
-//                        .applyBatch(ContactsProvider.EMAIL_AUTHORITY, ops);
-//            } 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
-//            }
+            // Execute these all at once...
+            ops.execute();
+
+            if (ops.mResults != null) {
+                ContentValues cv = new ContentValues();
+                cv.put(RawContacts.DIRTY, 0);
+                for (int i = 0; i < ops.mContactIndexCount; i++) {
+                    int index = ops.mContactIndexArray[i];
+                    Uri u = ops.mResults[index].uri;
+                    if (u != null) {
+                        String idString = u.getLastPathSegment();
+                        mService.mContentResolver.update(RawContacts.CONTENT_URI, cv,
+                                RawContacts._ID + "=" + idString, null);
+                    }
+                }
+            }
+
+            // Update the sync key in the database
+            mService.userLog("Contacts SyncKey saved as: " + mMailbox.mSyncKey);
+            ContentValues cv = new ContentValues();
+            cv.put(MailboxColumns.SYNC_KEY, mMailbox.mSyncKey);
+            Mailbox.update(mContext, Mailbox.CONTENT_URI, mMailbox.mId, cv);
 
             mService.userLog("Contacts SyncKey confirmed as: " + mMailbox.mSyncKey);
         }
     }
 
+
+    private Uri uriWithAccount(Uri uri) {
+        return uri.buildUpon()
+            .appendQueryParameter(RawContacts.ACCOUNT_NAME, mService.mAccount.mEmailAddress)
+            .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+            .build();
+    }
+
+    /**
+     * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
+     * ContentProvider.  It has, in addition to the Builder, ContentValues which, if present,
+     * represent the current values of that row, that can be compared against current values to
+     * see whether an update is even necessary.  The methods on SmartBuilder are delegated to
+     * the Builder.
+     */
+    private class SmartBuilder {
+        Builder builder;
+        ContentValues cv;
+
+        public SmartBuilder(Builder _builder) {
+            builder = _builder;
+        }
+
+        public SmartBuilder(Builder _builder, NamedContentValues _ncv) {
+            builder = _builder;
+            cv = _ncv.values;
+        }
+
+        SmartBuilder withValues(ContentValues values) {
+            builder.withValues(values);
+            return this;
+        }
+
+        SmartBuilder withValueBackReference(String key, int previousResult) {
+            builder.withValueBackReference(key, previousResult);
+            return this;
+        }
+
+        ContentProviderOperation build() {
+            return builder.build();
+        }
+
+        SmartBuilder withValue(String key, Object value) {
+            builder.withValue(key, value);
+            return this;
+        }
+    }
+
+    private class ContactOperations extends ArrayList<ContentProviderOperation> {
+        private static final long serialVersionUID = 1L;
+        private int mCount = 0;
+        private int mContactBackValue = mCount;
+        private int[] mContactIndexArray = new int[10];
+        private int mContactIndexCount = 0;
+        private ContentProviderResult[] mResults = null;
+
+        @Override
+        public boolean add(ContentProviderOperation op) {
+            super.add(op);
+            mCount++;
+            return true;
+        }
+
+        public void newContact(String serverId) {
+            Builder builder = ContentProviderOperation
+                .newInsert(uriWithAccount(RawContacts.CONTENT_URI));
+            ContentValues values = new ContentValues();
+            values.put(RawContacts.SOURCE_ID, serverId);
+            builder.withValues(values);
+            mContactBackValue = mCount;
+            mContactIndexArray[mContactIndexCount++] = mCount;
+            add(builder.build());
+        }
+
+        public void delete(long id) {
+            add(ContentProviderOperation.newDelete(ContentUris
+                    .withAppendedId(RawContacts.CONTENT_URI, id)).build());
+        }
+
+        public void execute() {
+            try {
+                mService.userLog("Executing " + size() + " CPO's");
+                mResults = mService.mContext.getContentResolver()
+                    .applyBatch(ContactsContract.AUTHORITY, this);
+            } catch (RemoteException e) {
+                // There is nothing sensible to be done here
+                Log.e(TAG, "problem inserting contact during server update", e);
+            } catch (OperationApplicationException e) {
+                // There is nothing sensible to be done here
+                Log.e(TAG, "problem inserting contact during server update", e);
+            }
+        }
+
+        /**
+         * Generate the uri for the data row associated with this NamedContentValues object
+         * @param ncv the NamedContentValues object
+         * @return a uri that can be used to refer to this row
+         */
+        private Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
+            long id = ncv.values.getAsLong(RawContacts._ID);
+            Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
+            return dataUri;
+        }
+
+        /**
+         * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
+         * tries to find a match, returning it
+         * @param list the list of NCV's from the contact entity
+         * @param contentItemType the mime type we're looking for
+         * @param type the subtype (e.g. HOME, WORK, etc.)
+         * @return the matching NCV or null if not found
+         */
+        private NamedContentValues findExistingData(ArrayList<NamedContentValues> list,
+                String contentItemType, int type) {
+            NamedContentValues result = null;
+
+            // Loop through the ncv's, looking for an existing row
+            for (NamedContentValues namedContentValues: list) {
+                Uri uri = namedContentValues.uri;
+                ContentValues cv = namedContentValues.values;
+                if (Data.CONTENT_URI.equals(uri)) {
+                    String mimeType = cv.getAsString(Data.MIMETYPE);
+                    if (mimeType.equals(contentItemType)) {
+                        if (type < 0 || cv.getAsInteger(Email.TYPE) == type) {
+                            result = namedContentValues;
+                        }
+                    }
+                }
+            }
+
+            // If we've found an existing data row, we'll delete it.  Any rows left at the
+            // end should be deleted...
+            if (result != null) {
+                list.remove(result);
+            }
+
+            // Return the row found (or null)
+            return result;
+        }
+
+        /**
+         * Create a wrapper for a builder (insert or update) that also includes the NCV for
+         * an existing row of this type.   If the SmartBuilder's cv field is not null, then
+         * it represents the current (old) values of this field.  The caller can then check
+         * whether the field is now different and needs to be updated; if it's not different,
+         * the caller will simply return and not generate a new CPO.  Otherwise, the builder
+         * should have its content values set, and the built CPO should be added to the
+         * ContactOperations list.
+         *
+         * @param entity the contact entity (or null if this is a new contact)
+         * @param mimeType the mime type of this row
+         * @param type the subtype of this row
+         * @return the created SmartBuilder
+         */
+        public SmartBuilder createBuilder(Entity entity, String mimeType, int type) {
+            int contactId = mContactBackValue;
+            SmartBuilder builder = null;
+
+            if (entity != null) {
+                NamedContentValues ncv =
+                    findExistingData(entity.getSubValues(), mimeType, type);
+                if (ncv != null) {
+                    builder = new SmartBuilder(
+                            ContentProviderOperation
+                                .newUpdate(dataUriFromNamedContentValues(ncv)),
+                            ncv);
+                } else {
+                    contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
+                }
+            }
+
+            if (builder == null) {
+                builder =
+                    new SmartBuilder(ContentProviderOperation.newInsert(Data.CONTENT_URI));
+                if (entity == null) {
+                    builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
+                } else {
+                    builder.withValue(Data.RAW_CONTACT_ID, contactId);
+                }
+
+                builder.withValue(Data.MIMETYPE, mimeType);
+            }
+
+            // Return the appropriate builder (insert or update)
+            // Caller will fill in the appropriate values; note MIMETYPE is already set
+            return builder;
+        }
+
+        /**
+         * Compare a column in a ContentValues with an (old) value, and see if they are the
+         * same.  For this purpose, null and an empty string are considered the same.
+         * @param cv a ContentValues object, from a NamedContentValues
+         * @param column a column that might be in the ContentValues
+         * @param oldValue an old value (or null) to check against
+         * @return whether the column's value in the ContentValues matches oldValue
+         */
+        private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
+            if (cv.containsKey(column)) {
+                if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
+                    return true;
+                }
+            } else if (oldValue == null || oldValue.length() == 0) {
+                return true;
+            }
+            return false;
+        }
+
+        public void addEmail(Entity entity, int type, String email) {
+            SmartBuilder builder = createBuilder(entity, Email.CONTENT_ITEM_TYPE, type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Email.DATA, email)) {
+                return;
+            }
+            builder.withValue(Email.TYPE, type);
+            builder.withValue(Email.DATA, email);
+            add(builder.build());
+        }
+
+        public void addName(Entity entity, String givenName, String familyName,
+                String displayName) {
+            SmartBuilder builder = createBuilder(entity, StructuredName.CONTENT_ITEM_TYPE, -1);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
+                    cvCompareString(cv, StructuredName.FAMILY_NAME, familyName)) {
+                return;
+            }
+            builder.withValue(StructuredName.GIVEN_NAME, givenName);
+            builder.withValue(StructuredName.FAMILY_NAME, familyName);
+            add(builder.build());
+        }
+
+        public void addPhoto() {
+//            final int photoRes = Generator.pickRandom(PHOTO_POOL);
+//
+//            Builder builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+//            builder.withValueBackReference(Data.CONTACT_ID, 0);
+//            builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+//            builder.withValue(Photo.PHOTO, getPhotoBytes(photoRes));
+//
+//            this.add(builder.build());
+        }
+
+        public void addPhone(Entity entity, int type, String phone) {
+            SmartBuilder builder = createBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
+                return;
+            }
+            builder.withValue(Phone.TYPE, type);
+            builder.withValue(Phone.NUMBER, phone);
+            add(builder.build());
+        }
+
+        public void addPostal(Entity entity, int type, String street, String city, String state,
+                String country, String code) {
+            SmartBuilder builder = createBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
+                    type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
+                    cvCompareString(cv, StructuredPostal.STREET, street) &&
+                    cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
+                    cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
+                    cvCompareString(cv, StructuredPostal.REGION, state)) {
+                return;
+            }
+            builder.withValue(StructuredPostal.TYPE, type);
+            builder.withValue(StructuredPostal.CITY, city);
+            builder.withValue(StructuredPostal.STREET, street);
+            builder.withValue(StructuredPostal.COUNTRY, country);
+            builder.withValue(StructuredPostal.POSTCODE, code);
+            builder.withValue(StructuredPostal.REGION, state);
+            add(builder.build());
+        }
+
+        public void addIm(Entity entity, int type, String account) {
+            SmartBuilder builder = createBuilder(entity, Im.CONTENT_ITEM_TYPE, type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Im.DATA, account)) {
+                return;
+            }
+            builder.withValue(Im.TYPE, type);
+            builder.withValue(Im.DATA, account);
+            add(builder.build());
+        }
+
+        public void addOrganization(Entity entity, int type, String company, String title) {
+            SmartBuilder builder = createBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
+                    cvCompareString(cv, Organization.TITLE, title)) {
+                return;
+            }
+            builder.withValue(Organization.TYPE, type);
+            builder.withValue(Organization.COMPANY, company);
+            builder.withValue(Organization.TITLE, title);
+            add(builder.build());
+        }
+
+        // TODO
+        public void addNote(String note) {
+            Builder builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+            builder.withValueBackReference(Data.RAW_CONTACT_ID, mContactBackValue);
+            builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
+            builder.withValue(Note.NOTE, note);
+            add(builder.build());
+        }
+
+        public void addExtras(Entity entity, String extras) {
+            SmartBuilder builder = createBuilder(entity, Extras.CONTENT_ITEM_TYPE, -1);
+            ContentValues cv = builder.cv;
+            if (cv != null && cvCompareString(cv, Extras.EXTRAS, extras)) {
+                return;
+            }
+            builder.withValue(Extras.EXTRAS, extras);
+            add(builder.build());
+        }
+    }
+
     @Override
     public void cleanup(EasSyncService service) {
+        // Mark the changed contacts dirty = 0
+        // TODO Put this in a single batch
+        ContactOperations ops = new ContactOperations();
+        for (Long id: mUpdatedIdList) {
+            ops.add(ContentProviderOperation
+                    .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id))
+                    .withValue(RawContacts.DIRTY, 0).build());
+        }
+
+        ops.execute();
     }
 
     @Override
@@ -382,8 +850,206 @@
         return "Contacts";
     }
 
+    private void sendEmail(EasSerializer s, ContentValues cv) throws IOException {
+        String value = cv.getAsString(Email.DATA);
+        switch (cv.getAsInteger(Email.TYPE)) {
+            case TYPE_EMAIL1:
+                s.data("Email1Address", value);
+                break;
+            case TYPE_EMAIL2:
+                s.data("Email2Address", value);
+                break;
+            case TYPE_EMAIL3:
+                s.data("Email3Address", value);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void sendIm(EasSerializer s, ContentValues cv) throws IOException {
+        String value = cv.getAsString(Email.DATA);
+        switch (cv.getAsInteger(Email.TYPE)) {
+            case TYPE_IM1:
+                s.data("IMAddress", value);
+                break;
+            case TYPE_IM2:
+                s.data("IMAddress2", value);
+                break;
+            case TYPE_IM3:
+                s.data("IMAddress3", value);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void sendOnePostal(EasSerializer s, ContentValues cv, String[] fieldNames)
+            throws IOException{
+        if (cv.containsKey(StructuredPostal.CITY)) {
+            s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY));
+        }
+        if (cv.containsKey(StructuredPostal.COUNTRY)) {
+            s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY));
+        }
+        if (cv.containsKey(StructuredPostal.POSTCODE)) {
+            s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE));
+        }
+        if (cv.containsKey(StructuredPostal.REGION)) {
+            s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION));
+        }
+        if (cv.containsKey(StructuredPostal.STREET)) {
+            s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET));
+        }
+    }
+
+    private void sendStructuredPostal(EasSerializer s, ContentValues cv) throws IOException {
+        switch (cv.getAsInteger(StructuredPostal.TYPE)) {
+            case StructuredPostal.TYPE_HOME:
+                sendOnePostal(s, cv, new String[] {"HomeAddressCity", "HomeAddressCountry",
+                        "HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet"});
+                break;
+            case StructuredPostal.TYPE_WORK:
+                sendOnePostal(s, cv, new String[] {"BusinessAddressCity", "BusinessAddressCountry",
+                        "BusinessAddressPostalCode", "BusinessAddressState",
+                        "BusinessAddressStreet"});
+                break;
+            case StructuredPostal.TYPE_OTHER:
+                sendOnePostal(s, cv, new String[] {"OtherAddressCity", "OtherAddressCountry",
+                        "OtherAddressPostalCode", "OtherAddressState", "OtherAddressStreet"});
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void sendStructuredName(EasSerializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(StructuredName.FAMILY_NAME)) {
+            s.data("LastName", cv.getAsString(StructuredName.FAMILY_NAME));
+        }
+        if (cv.containsKey(StructuredName.GIVEN_NAME)) {
+            s.data("FirstName", cv.getAsString(StructuredName.GIVEN_NAME));
+        }
+    }
+
+    private void sendOrganization(EasSerializer s, ContentValues cv) throws IOException {
+        if (cv.containsKey(Organization.TITLE)) {
+            s.data("JobTitle", cv.getAsString(Organization.TITLE));
+        }
+        if (cv.containsKey(Organization.COMPANY)) {
+            s.data("CompanyName", cv.getAsString(Organization.COMPANY));
+        }
+    }
+
+    private void sendPhone(EasSerializer s, ContentValues cv) throws IOException {
+        String value = cv.getAsString(Phone.NUMBER);
+        switch (cv.getAsInteger(Phone.TYPE)) {
+            case TYPE_WORK2:
+                s.data("Business2TelephoneNumber", value);
+                break;
+            case Phone.TYPE_WORK:
+                s.data("BusinessTelephoneNumber", value);
+                break;
+            case TYPE_MMS:
+                s.data("MMS", value);
+                break;
+            case Phone.TYPE_FAX_WORK:
+                s.data("BusinessFaxNumber", value);
+                break;
+            case TYPE_COMPANY_MAIN:
+                s.data("CompanyMainPhone", value);
+                break;
+            case Phone.TYPE_HOME:
+                s.data("HomeTelephoneNumber", value);
+                break;
+            case TYPE_HOME2:
+                s.data("Home2TelephoneNumber", value);
+                break;
+            case Phone.TYPE_MOBILE:
+                s.data("MobileTelephoneNumber", value);
+                break;
+            case TYPE_CAR:
+                s.data("CarTelephoneNumber", value);
+                break;
+            case Phone.TYPE_PAGER:
+                s.data("PagerNumber", value);
+                break;
+            case TYPE_RADIO:
+                s.data("RadioTelephoneNumber", value);
+                break;
+            case Phone.TYPE_FAX_HOME:
+                s.data("HomeFaxNumber", value);
+                break;
+            case TYPE_EMAIL2:
+                s.data("Email2Address", value);
+                break;
+            case TYPE_EMAIL3:
+                s.data("Email3Address", value);
+                break;
+            default:
+                break;
+        }
+    }
+
     @Override
     public boolean sendLocalChanges(EasSerializer s, EasSyncService service) throws IOException {
+        // First, let's find Contacts that have changed.
+        ContentResolver cr = service.mContentResolver;
+        Uri uri = RawContacts.CONTENT_URI.buildUpon()
+                .appendQueryParameter(RawContacts.ACCOUNT_NAME, service.mAccount.mEmailAddress)
+                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+                .build();
+
+        try {
+            // Get them all atomically
+            //EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
+            EntityIterator ei = cr.queryEntities(uri, null, null, null);
+            boolean first = true;
+            while (ei.hasNext()) {
+                Entity entity = ei.next();
+                // For each of these entities, create the change commands
+                ContentValues entityValues = entity.getEntityValues();
+                String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
+                if (first) {
+                    s.start("Commands");
+                    first = false;
+                }
+                s.start("Change").data("ServerId", serverId).start("ApplicationData");
+                // Write out the data here
+                for (NamedContentValues ncv: entity.getSubValues()) {
+                    ContentValues cv = ncv.values;
+                    String mimeType = cv.getAsString(Data.MIMETYPE);
+                    if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
+                        sendEmail(s, cv);
+                    } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
+                        sendPhone(s, cv);
+                    } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
+                        sendStructuredName(s, cv);
+                    } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
+                        sendStructuredPostal(s, cv);
+                    } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
+                        sendOrganization(s, cv);
+                    } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
+                        sendIm(s, cv);
+                    } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
+
+                    } else if (mimeType.equals(Extras.CONTENT_ITEM_TYPE)) {
+
+                    } else {
+                        mService.userLog("Contacts upsync, unknown data: " + mimeType);
+                    }
+                }
+                s.end("ApplicationData").end("Change");
+                mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
+            }
+           if (!first) {
+                s.end("Commands");
+            }
+
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not read dirty contacts.");
+        }
+
         return false;
     }
 }
diff --git a/src/com/android/exchange/adapter/EasEmailSyncAdapter.java b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java
index d502f52..8fdb553 100644
--- a/src/com/android/exchange/adapter/EasEmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EasEmailSyncAdapter.java
@@ -63,8 +63,8 @@
     ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
     ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
 
-    public EasEmailSyncAdapter(Mailbox mailbox) {
-        super(mailbox);
+    public EasEmailSyncAdapter(Mailbox mailbox, EasSyncService service) {
+        super(mailbox, service);
     }
 
     @Override
@@ -72,10 +72,10 @@
         EasEmailSyncParser p = new EasEmailSyncParser(is, service);
         return p.parse();
     }
-    
+
     public class EasEmailSyncParser extends EasContentParser {
 
-        private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = 
+        private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
             SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
 
         private String mMailboxIdAsString;
@@ -88,6 +88,7 @@
             }
         }
 
+        @Override
         public void wipe() {
             mContentResolver.delete(Message.CONTENT_URI,
                     Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
@@ -174,7 +175,7 @@
 
             while (nextTag(EasTags.SYNC_ADD) != END) {
                 switch (tag) {
-                    case EasTags.SYNC_SERVER_ID: 
+                    case EasTags.SYNC_SERVER_ID:
                         msg.mServerId = getValue();
                         break;
                     case EasTags.SYNC_APPLICATION_DATA:
@@ -389,6 +390,7 @@
         /* (non-Javadoc)
          * @see com.android.exchange.adapter.EasContentParser#commandsParser()
          */
+        @Override
         public void commandsParser() throws IOException {
             ArrayList<Message> newEmails = new ArrayList<Message>();
             ArrayList<Long> deletedEmails = new ArrayList<Long>();
@@ -436,7 +438,7 @@
                     mMailbox.toContentValues()).build());
 
             addCleanupOps(ops);
-            
+
             try {
                 mService.mContext.getContentResolver()
                         .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
diff --git a/src/com/android/exchange/adapter/EasFolderSyncParser.java b/src/com/android/exchange/adapter/EasFolderSyncParser.java
index 3d68694..ea9df34 100644
--- a/src/com/android/exchange/adapter/EasFolderSyncParser.java
+++ b/src/com/android/exchange/adapter/EasFolderSyncParser.java
@@ -51,7 +51,7 @@
 
 public class EasFolderSyncParser extends EasParser {
 
-    private static boolean DEBUG_LOGGING = false;
+    private static boolean DEBUG_LOGGING = true;
 
     public static final String TAG = "FolderSyncParser";
 
diff --git a/src/com/android/exchange/adapter/EasSyncAdapter.java b/src/com/android/exchange/adapter/EasSyncAdapter.java
index cf06d86..f79b68b 100644
--- a/src/com/android/exchange/adapter/EasSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EasSyncAdapter.java
@@ -29,6 +29,7 @@
  */
 public abstract class EasSyncAdapter {
     public Mailbox mMailbox;
+    public EasSyncService mService;
 
     // Create the data for local changes that need to be sent up to the server
     public abstract boolean sendLocalChanges(EasSerializer s, EasSyncService service) 
@@ -41,8 +42,9 @@
     public abstract String getCollectionName();
     public abstract void cleanup(EasSyncService service);
 
-    public EasSyncAdapter(Mailbox mailbox) {
+    public EasSyncAdapter(Mailbox mailbox, EasSyncService service) {
         mMailbox = mailbox;
+        mService = service;
     }
 }
 
diff --git a/src/com/android/exchange/adapter/EasTags.java b/src/com/android/exchange/adapter/EasTags.java
index 0f330e5..96de3fc 100644
--- a/src/com/android/exchange/adapter/EasTags.java
+++ b/src/com/android/exchange/adapter/EasTags.java
@@ -329,8 +329,9 @@
         },
         {
             // 0x01 Contacts
-            "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "Body",
-            "BodySize", "BodyTruncated", "Business2TelephoneNumber", "BusinessAddressCity",
+            "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "ContactsBody",
+            "ContactsBodySize", "ContactsBodyTruncated", "Business2TelephoneNumber",
+            "BusinessAddressCity",
             "BusinessAddressCountry", "BusinessAddressPostalCode", "BusinessAddressState",
             "BusinessAddressStreet", "BusinessFaxNumber", "BusinessTelephoneNumber",
             "CarTelephoneNumber", "ContactsCategories", "ContactsCategory", "Children", "Child",
@@ -338,8 +339,8 @@
             "FileAs", "FirstName", "Home2TelephoneNumber", "HomeAddressCity", "HomeAddressCountry",
             "HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet", "HomeFaxNumber",
             "HomeTelephoneNumber", "JobTitle", "LastName", "MiddleName", "MobileTelephoneNumber",
-            "OfficeLocation", "OfficeAddressCity", "OfficeAddressCountry",
-            "OfficeAddressPostalCode", "OfficeAddressState", "OfficeAddressStreet", "PagerNumber",
+            "OfficeLocation", "OtherAddressCity", "OtherAddressCountry",
+            "OtherAddressPostalCode", "OtherAddressState", "OtherAddressStreet", "PagerNumber",
             "RadioTelephoneNumber", "Spouse", "Suffix", "Title", "Webpage", "YomiCompanyName",
             "YomiFirstName", "YomiLastName", "CompressedRTF", "Picture"
         },
@@ -354,7 +355,7 @@
             "Recurrence_Occurrences", "Recurrence_Interval", "Recurrence_DayOfWeek",
             "Recurrence_DayOfMonth", "Recurrence_WeekOfMonth", "Recurrence_MonthOfYear",
             "StartTime", "Sensitivity", "TimeZone", "GlobalObjId", "ThreadTopic", "MIMEData",
-            "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "ContentClass",
+            "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "EmailContentClass",
             "FlagType", "CompleteTime"
         },
         {
@@ -375,7 +376,7 @@
         },
         {
             // 0x05 Move
-            "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "Response", "Status",
+            "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "MoveResponse", "MoveStatus",
             "DstMsgId"
         },
         {
@@ -384,9 +385,9 @@
         {
             // 0x07 FolderHierarchy
             "Folders", "Folder", "FolderDisplayName", "FolderServerId", "FolderParentId", "Type",
-            "Response", "Status", "ContentClass", "Changes", "FolderAdd", "FolderDelete",
-            "FolderUpdate", "FolderSyncKey", "FolderCreate", "FolderDelete", "FolderUpdate",
-            "FolderSync", "Count", "Version"
+            "FolderResponse", "FolderStatus", "FolderContentClass", "Changes", "FolderAdd",
+            "FolderDelete", "FolderUpdate", "FolderSyncKey", "FolderFolderCreate",
+            "FolderFolderDelete", "FolderFolderUpdate", "FolderSync", "Count", "FolderVersion"
         },
         {
             // 0x08 MeetingResponse
@@ -407,12 +408,12 @@
         },
         {
             // 0x0D Ping
-            "Ping", "AutdState", "Status", "HeartbeatInterval", "PingFolders", "PingFolder",
+            "Ping", "AutdState", "PingStatus", "HeartbeatInterval", "PingFolders", "PingFolder",
             "PingId", "PingClass", "MaxFolders"
         },
         {
             // 0x0E Provision
-            "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "Status",
+            "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "ProvisionStatus",
             "RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled",
             "AlphanumericDevicePasswordRequired",
             "DeviceEncryptionEnabled", "-unused-", "AttachmentsEnabled", "MinDevicePasswordLength",
@@ -436,8 +437,8 @@
         },
         {
             // 0x10 Gal
-            "DisplayName", "Phone", "Office", "Title", "Company", "Alias", "FirstName", "LastName",
-            "HomePhone", "MobilePhone", "EmailAddress"
+            "GalDisplayName", "GalPhone", "GalOffice", "GalTitle", "GalCompany", "GalAlias",
+            "GalFirstName", "GalLastName", "GalHomePhone", "GalMobilePhone", "GalEmailAddress"
         },
         {
             // 0x11 AirSyncBase
diff --git a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
index 21b7284..fab5551 100644
--- a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
@@ -51,7 +51,7 @@
         service.mContext = getContext();
         service.mMailbox = mailbox;
         service.mAccount = account;
-        EasEmailSyncAdapter adapter = new EasEmailSyncAdapter(mailbox);
+        EasEmailSyncAdapter adapter = new EasEmailSyncAdapter(mailbox, service);
         EasEmailSyncParser p;
         p = adapter.new EasEmailSyncParser(getTestInputStream(), service);
         // Test a few known types
diff --git a/tests/src/com/android/exchange/EasTagsTests.java b/tests/src/com/android/exchange/EasTagsTests.java
new file mode 100644
index 0000000..8bd4cda
--- /dev/null
+++ b/tests/src/com/android/exchange/EasTagsTests.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2009 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;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.adapter.EasEmailSyncAdapter;
+import com.android.exchange.adapter.EasTags;
+import com.android.exchange.adapter.EasEmailSyncAdapter.EasEmailSyncParser;
+
+import android.test.AndroidTestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+
+public class EasTagsTests extends AndroidTestCase {
+
+    // Make sure there are no duplicates in the tags table
+    public void testNoDuplicates() {
+        String[][] allTags = EasTags.pages;
+        HashMap<String, Boolean> map = new HashMap<String, Boolean>();
+        for (String[] page: allTags) {
+            for (String tag: page) {
+                assertTrue(!map.containsKey(tag));
+                map.put(tag, true);
+            }
+        }
+    }
+}