Hook up logic for re-parenting of user edits, unit tests.

When persisting edits, we assert that RawContacts.VERSION
has remained consistent.  When this check fails, it usually
means that a server change has changed the underlying data,
and instead of dropping user edits, we "re-parent" their
changes after re-reading the base data.

This change finishes up the re-parenting logic to run over
an entire EntitySet, and also has a nice pile of unit tests
to verify behavior.  Fixes http://b/2115136
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index f51ea34..8e0cf9f 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -59,6 +59,9 @@
 public class EntityDelta implements Parcelable {
     // TODO: optimize by using contentvalues pool, since we allocate so many of them
 
+    private static final String TAG = "EntityDelta";
+    private static final boolean LOGV = true;
+
     /**
      * Direct values from {@link Entity#getEntityValues()}.
      */
@@ -97,28 +100,41 @@
      * existing "after" states. This is typically used when re-parenting changes
      * onto an updated {@link Entity}.
      */
-    public void mergeAfter(EntityDelta remote) {
-        // Always take after values from new state
-        this.mValues.mAfter = remote.mValues.mAfter;
+    public static EntityDelta mergeAfter(EntityDelta local, EntityDelta remote) {
+        // Bail early if trying to merge delete with missing local
+        if (local == null && remote.mValues.isDelete()) return null;
 
-        // TODO: log before/after versions to track re-parenting
+        // Create local version if none exists yet
+        if (local == null) local = new EntityDelta();
+
+        if (LOGV) {
+            final Long localVersion = (local.mValues == null) ? -1 : local.mValues
+                    .getAsLong(RawContacts.VERSION);
+            final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
+            Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
+                    + localVersion);
+        }
+
+        // Create values if needed, and merge "after" changes
+        local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
 
         // Find matching local entry for each remote values, or create
         for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
             for (ValuesDelta remoteEntry : mimeEntries) {
                 final Long childId = remoteEntry.getId();
 
-                ValuesDelta localEntry = this.getEntry(childId);
-                if (localEntry == null) {
-                    // Is "insert", or "before" record is missing, so now "insert"
-                    localEntry = ValuesDelta.fromAfter(remoteEntry.mAfter);
-                    this.addEntry(localEntry);
-                } else {
-                    // Existing entry "update"
-                    localEntry.mAfter = remoteEntry.mAfter;
+                // Find or create local match and merge
+                final ValuesDelta localEntry = local.getEntry(childId);
+                final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
+
+                if (localEntry == null && merged != null) {
+                    // No local entry before, so insert
+                    local.addEntry(merged);
                 }
             }
         }
+
+        return local;
     }
 
     public ValuesDelta getValues() {
@@ -181,9 +197,10 @@
         return mEntries.containsKey(mimeType);
     }
 
-    public void addEntry(ValuesDelta entry) {
+    public ValuesDelta addEntry(ValuesDelta entry) {
         final String mimeType = entry.getMimetype();
         getMimeEntries(mimeType, true).add(entry);
+        return entry;
     }
 
     /**
@@ -587,6 +604,44 @@
             return keys;
         }
 
+        /**
+         * Return complete set of "before" and "after" values mixed together,
+         * giving full state regardless of edits.
+         */
+        public ContentValues getCompleteValues() {
+            final ContentValues values = new ContentValues();
+            if (mBefore != null) {
+                values.putAll(mBefore);
+            }
+            if (mAfter != null) {
+                values.putAll(mAfter);
+            }
+            return values;
+        }
+
+        /**
+         * Merge the "after" values from the given {@link ValuesDelta},
+         * discarding any existing "after" state. This is typically used when
+         * re-parenting changes onto an updated {@link Entity}.
+         */
+        public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) {
+            // Bail early if trying to merge delete with missing local
+            if (local == null && remote.isDelete()) return null;
+
+            // Create local version if none exists yet
+            if (local == null) local = new ValuesDelta();
+
+            if (!local.beforeExists()) {
+                // Any "before" record is missing, so take all values as "insert"
+                local.mAfter = remote.getCompleteValues();
+            } else {
+                // Existing "update" with only "after" values
+                local.mAfter = remote.mAfter;
+            }
+
+            return local;
+        }
+
         @Override
         public boolean equals(Object object) {
             if (object instanceof ValuesDelta) {
diff --git a/src/com/android/contacts/model/EntitySet.java b/src/com/android/contacts/model/EntitySet.java
index 75c9dc3..02a127e 100644
--- a/src/com/android/contacts/model/EntitySet.java
+++ b/src/com/android/contacts/model/EntitySet.java
@@ -21,6 +21,7 @@
 
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Entity;
 import android.content.EntityIterator;
 import android.content.ContentProviderOperation.Builder;
@@ -30,6 +31,7 @@
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
 
 import java.util.ArrayList;
 
@@ -82,11 +84,28 @@
     }
 
     /**
-     * Merge the "after" values from the given {@link EntitySet}.
+     * Merge the "after" values from the given {@link EntitySet}, discarding any
+     * previous "after" states. This is typically used when re-parenting user
+     * edits onto an updated {@link EntitySet}.
      */
-    public void mergeAfter(EntitySet remote) {
-        // TODO: write this folding logic to re-parent
-        throw new UnsupportedOperationException();
+    public static EntitySet mergeAfter(EntitySet local, EntitySet remote) {
+        if (local == null) local = new EntitySet();
+
+        // For each entity in the remote set, try matching over existing
+        for (EntityDelta remoteEntity : remote) {
+            final Long rawContactId = remoteEntity.getValues().getId();
+
+            // Find or create local match and merge
+            final EntityDelta localEntity = local.getByRawContactId(rawContactId);
+            final EntityDelta merged = EntityDelta.mergeAfter(localEntity, remoteEntity);
+
+            if (localEntity == null && merged != null) {
+                // No local entry before, so insert
+                local.add(merged);
+            }
+        }
+
+        return local;
     }
 
     /**
@@ -173,7 +192,7 @@
     /**
      * Find {@link RawContacts#_ID} of the requested {@link EntityDelta}.
      */
-    public long getRawContactId(int index) {
+    public Long getRawContactId(int index) {
         if (index >= 0 && index < this.size()) {
             final EntityDelta delta = this.get(index);
             final ValuesDelta values = delta.getValues();
@@ -181,16 +200,22 @@
                 return values.getAsLong(RawContacts._ID);
             }
         }
-        return 0;
+        return null;
+    }
+
+    public EntityDelta getByRawContactId(Long rawContactId) {
+        final int index = this.indexOfRawContactId(rawContactId);
+        return (index == -1) ? null : this.get(index);
     }
 
     /**
      * Find index of given {@link RawContacts#_ID} when present.
      */
     public int indexOfRawContactId(Long rawContactId) {
+        if (rawContactId == null) return -1;
         final int size = this.size();
         for (int i = 0; i < size; i++) {
-            final long currentId = getRawContactId(i);
+            final Long currentId = getRawContactId(i);
             if (currentId == rawContactId) {
                 return i;
             }
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index c02ee80..c59c1d1 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -574,8 +574,7 @@
                     Log.w(TAG, "Version consistency failed, re-parenting", e);
                     final EntitySet newState = EntitySet.fromQuery(resolver,
                             target.mQuerySelection, null, null);
-                    newState.mergeAfter(state);
-                    state = newState;
+                    state = EntitySet.mergeAfter(newState, state);
                 }
             }
 
diff --git a/tests/src/com/android/contacts/EntityDeltaTests.java b/tests/src/com/android/contacts/EntityDeltaTests.java
index 73318cb..70a506b 100644
--- a/tests/src/com/android/contacts/EntityDeltaTests.java
+++ b/tests/src/com/android/contacts/EntityDeltaTests.java
@@ -93,8 +93,8 @@
         final EntityDelta dest = EntityDelta.fromBefore(before);
 
         // Merge modified values and assert they match
-        dest.mergeAfter(source);
-        assertEquals("Unexpected change when merging", source, dest);
+        final EntityDelta merged = EntityDelta.mergeAfter(dest, source);
+        assertEquals("Unexpected change when merging", source, merged);
     }
 
     public void testParcelChangesInsert() {
@@ -104,14 +104,14 @@
 
         // Add a new row and pass across parcel, should be same
         final ContentValues phone = new ContentValues();
-        phone.put(Data.MIMETYPE, Phone.MIMETYPE);
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
         phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
         phone.put(Phone.TYPE, Phone.TYPE_WORK);
         source.addEntry(ValuesDelta.fromAfter(phone));
 
         // Merge modified values and assert they match
-        dest.mergeAfter(source);
-        assertEquals("Unexpected change when merging", source, dest);
+        final EntityDelta merged = EntityDelta.mergeAfter(dest, source);
+        assertEquals("Unexpected change when merging", source, merged);
     }
 
     public void testParcelChangesUpdate() {
@@ -124,8 +124,8 @@
         child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
 
         // Merge modified values and assert they match
-        dest.mergeAfter(source);
-        assertEquals("Unexpected change when merging", source, dest);
+        final EntityDelta merged = EntityDelta.mergeAfter(dest, source);
+        assertEquals("Unexpected change when merging", source, merged);
     }
 
     public void testParcelChangesDelete() {
@@ -138,8 +138,8 @@
         child.markDeleted();
 
         // Merge modified values and assert they match
-        dest.mergeAfter(source);
-        assertEquals("Unexpected change when merging", source, dest);
+        final EntityDelta merged = EntityDelta.mergeAfter(dest, source);
+        assertEquals("Unexpected change when merging", source, merged);
     }
 
     /**
@@ -221,7 +221,7 @@
 
         // Insert a new phone number
         final ContentValues phone = new ContentValues();
-        phone.put(Data.MIMETYPE, Phone.MIMETYPE);
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
         phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
         phone.put(Phone.TYPE, Phone.TYPE_WORK);
         source.addEntry(ValuesDelta.fromAfter(phone));
@@ -261,7 +261,7 @@
 
         // Insert a new phone number
         final ContentValues phone = new ContentValues();
-        phone.put(Data.MIMETYPE, Phone.MIMETYPE);
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
         phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
         phone.put(Phone.TYPE, Phone.TYPE_WORK);
         source.addEntry(ValuesDelta.fromAfter(phone));
@@ -386,7 +386,7 @@
 
         // Insert a new phone number
         final ContentValues phone = new ContentValues();
-        phone.put(Data.MIMETYPE, Phone.MIMETYPE);
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
         phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
         phone.put(Phone.TYPE, Phone.TYPE_WORK);
         source.addEntry(ValuesDelta.fromAfter(phone));
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index 6bc3005..1de3936 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -26,7 +26,6 @@
 import com.android.contacts.model.EntitySet;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.google.android.collect.Lists;
@@ -40,7 +39,6 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
-import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -99,7 +97,9 @@
      * testing purposes.
      */
     protected ContactsSource getSource() {
-        return new MockContactsSource();
+        final ContactsSource source = new MockContactsSource();
+        source.ensureInflated(getContext(), ContactsSource.LEVEL_CONSTRAINTS);
+        return source;
     }
 
     /**
diff --git a/tests/src/com/android/contacts/EntitySetTests.java b/tests/src/com/android/contacts/EntitySetTests.java
index 89fb455..cf0257f 100644
--- a/tests/src/com/android/contacts/EntitySetTests.java
+++ b/tests/src/com/android/contacts/EntitySetTests.java
@@ -16,6 +16,11 @@
 
 package com.android.contacts;
 
+import static android.content.ContentProviderOperation.TYPE_ASSERT;
+import static android.content.ContentProviderOperation.TYPE_DELETE;
+import static android.content.ContentProviderOperation.TYPE_INSERT;
+import static android.content.ContentProviderOperation.TYPE_UPDATE;
+
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntitySet;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
@@ -23,10 +28,15 @@
 import android.content.ContentProviderOperation;
 import android.content.ContentValues;
 import android.content.Entity;
+import android.net.Uri;
+import android.provider.BaseColumns;
 import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
 
 import java.util.ArrayList;
 
@@ -41,6 +51,16 @@
     private static final long CONTACT_FIRST = 1;
     private static final long CONTACT_SECOND = 2;
 
+    public static final long CONTACT_BOB = 10;
+    public static final long CONTACT_MARY = 11;
+
+    public static final long PHONE_RED = 20;
+    public static final long PHONE_GREEN = 21;
+    public static final long PHONE_BLUE = 22;
+
+    public static final long VER_FIRST = 100;
+    public static final long VER_SECOND = 200;
+
     public EntitySetTests() {
         super();
     }
@@ -50,13 +70,13 @@
         mContext = getContext();
     }
 
-    protected EntityDelta getUpdate(long rawContactId) {
+    protected static EntityDelta getUpdate(long rawContactId) {
         final Entity before = EntityDeltaTests.getEntity(rawContactId,
                 EntityDeltaTests.TEST_PHONE_ID);
         return EntityDelta.fromBefore(before);
     }
 
-    protected EntityDelta getInsert() {
+    protected static EntityDelta getInsert() {
         final ContentValues after = new ContentValues();
         after.put(RawContacts.ACCOUNT_NAME, EntityDeltaTests.TEST_ACCOUNT_NAME);
         after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
@@ -65,7 +85,7 @@
         return new EntityDelta(values);
     }
 
-    protected EntitySet setFrom(EntityDelta... deltas) {
+    protected static EntitySet buildSet(EntityDelta... deltas) {
         final EntitySet set = EntitySet.fromSingle(deltas[0]);
         for (int i = 1; i < deltas.length; i++) {
             set.add(deltas[i]);
@@ -73,11 +93,104 @@
         return set;
     }
 
+    protected static EntityDelta buildBeforeEntity(long rawContactId, long version,
+            ContentValues... entries) {
+        // Build an existing contact read from database
+        final ContentValues contact = new ContentValues();
+        contact.put(RawContacts.VERSION, version);
+        contact.put(RawContacts._ID, rawContactId);
+        final Entity before = new Entity(contact);
+        for (ContentValues entry : entries) {
+            before.addSubValue(Data.CONTENT_URI, entry);
+        }
+        return EntityDelta.fromBefore(before);
+    }
+
+    protected static EntityDelta buildAfterEntity(ContentValues... entries) {
+        // Build an existing contact read from database
+        final ContentValues contact = new ContentValues();
+        contact.putNull(RawContacts.ACCOUNT_NAME);
+        final EntityDelta after = new EntityDelta(ValuesDelta.fromAfter(contact));
+        for (ContentValues entry : entries) {
+            after.addEntry(ValuesDelta.fromAfter(entry));
+        }
+        return after;
+    }
+
+    protected static ContentValues buildPhone(long phoneId) {
+        return buildPhone(phoneId, Long.toString(phoneId));
+    }
+
+    protected static ContentValues buildPhone(long phoneId, String value) {
+        final ContentValues values = new ContentValues();
+        values.put(Data._ID, phoneId);
+        values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        values.put(Phone.NUMBER, value);
+        values.put(Phone.TYPE, Phone.TYPE_HOME);
+        return values;
+    }
+
+    protected static void insertPhone(EntitySet set, long rawContactId, ContentValues values) {
+        final EntityDelta match = set.getByRawContactId(rawContactId);
+        match.addEntry(ValuesDelta.fromAfter(values));
+    }
+
+    protected static ValuesDelta getPhone(EntitySet set, long rawContactId, long dataId) {
+        final EntityDelta match = set.getByRawContactId(rawContactId);
+        return match.getEntry(dataId);
+    }
+
+    protected void assertDiffPattern(EntitySet set, ContentProviderOperation... pattern) {
+        final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+
+        assertEquals("Unexpected operations", pattern.length, diff.size());
+        for (int i = 0; i < pattern.length; i++) {
+            final ContentProviderOperation expected = pattern[i];
+            final ContentProviderOperation found = diff.get(i);
+
+            final String expectedType = getStringForType(expected.getType());
+            final String foundType = getStringForType(found.getType());
+
+            assertEquals("Unexpected type", expectedType, foundType);
+            assertEquals("Unexpected uri", expected.getUri(), found.getUri());
+        }
+    }
+
+    protected static String getStringForType(int type) {
+        switch (type) {
+            case TYPE_ASSERT: return "TYPE_ASSERT";
+            case TYPE_INSERT: return "TYPE_INSERT";
+            case TYPE_UPDATE: return "TYPE_UPDATE";
+            case TYPE_DELETE: return "TYPE_DELETE";
+            default: return Integer.toString(type);
+        }
+    }
+
+    protected static ContentProviderOperation buildOper(Uri uri, int type) {
+        final ContentValues values = new ContentValues();
+        values.put(BaseColumns._ID, 4);
+        switch (type) {
+            case TYPE_ASSERT:
+                return ContentProviderOperation.newAssertQuery(uri).withValues(values).build();
+            case TYPE_INSERT:
+                return ContentProviderOperation.newInsert(uri).withValues(values).build();
+            case TYPE_UPDATE:
+                return ContentProviderOperation.newUpdate(uri).withValues(values).build();
+            case TYPE_DELETE:
+                return ContentProviderOperation.newDelete(uri).build();
+        }
+        return null;
+    }
+
+    protected static Long getVersion(EntitySet set, Long rawContactId) {
+        return set.getByRawContactId(rawContactId).getValues().getAsLong(RawContacts.VERSION);
+    }
+
     /**
      * Count number of {@link AggregationExceptions} updates contained in the
      * given list of {@link ContentProviderOperation}.
      */
-    protected int countExceptionUpdates(ArrayList<ContentProviderOperation> diff) {
+    protected static int countExceptionUpdates(ArrayList<ContentProviderOperation> diff) {
         int updateCount = 0;
         for (ContentProviderOperation oper : diff) {
             if (AggregationExceptions.CONTENT_URI.equals(oper.getUri())
@@ -90,7 +203,7 @@
 
     public void testInsert() {
         final EntityDelta insert = getInsert();
-        final EntitySet set = setFrom(insert);
+        final EntitySet set = buildSet(insert);
 
         // Inserting single shouldn't create rules
         final ArrayList<ContentProviderOperation> diff = set.buildDiff();
@@ -101,7 +214,7 @@
     public void testUpdateUpdate() {
         final EntityDelta updateFirst = getUpdate(CONTACT_FIRST);
         final EntityDelta updateSecond = getUpdate(CONTACT_SECOND);
-        final EntitySet set = setFrom(updateFirst, updateSecond);
+        final EntitySet set = buildSet(updateFirst, updateSecond);
 
         // Updating two existing shouldn't create rules
         final ArrayList<ContentProviderOperation> diff = set.buildDiff();
@@ -112,7 +225,7 @@
     public void testUpdateInsert() {
         final EntityDelta update = getUpdate(CONTACT_FIRST);
         final EntityDelta insert = getInsert();
-        final EntitySet set = setFrom(update, insert);
+        final EntitySet set = buildSet(update, insert);
 
         // New insert should only create one rule
         final ArrayList<ContentProviderOperation> diff = set.buildDiff();
@@ -124,7 +237,7 @@
         final EntityDelta insertFirst = getInsert();
         final EntityDelta update = getUpdate(CONTACT_FIRST);
         final EntityDelta insertSecond = getInsert();
-        final EntitySet set = setFrom(insertFirst, update, insertSecond);
+        final EntitySet set = buildSet(insertFirst, update, insertSecond);
 
         // Two inserts should create two rules to bind against single existing
         final ArrayList<ContentProviderOperation> diff = set.buildDiff();
@@ -136,11 +249,189 @@
         final EntityDelta insertFirst = getInsert();
         final EntityDelta insertSecond = getInsert();
         final EntityDelta insertThird = getInsert();
-        final EntitySet set = setFrom(insertFirst, insertSecond, insertThird);
+        final EntitySet set = buildSet(insertFirst, insertSecond, insertThird);
 
         // Three new inserts should create only two binding rules
         final ArrayList<ContentProviderOperation> diff = set.buildDiff();
         final int exceptionCount = countExceptionUpdates(diff);
         assertEquals("Unexpected exception updates", 2, exceptionCount);
     }
+
+    public void testMergeDataRemoteInsert() {
+        final EntitySet first = buildSet(buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(buildBeforeEntity(CONTACT_BOB, VER_SECOND,
+                buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+        // Merge in second version, verify they match
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertEquals("Unexpected change when merging", second, merged);
+    }
+
+    public void testMergeDataLocalUpdateRemoteInsert() {
+        final EntitySet first = buildSet(buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(buildBeforeEntity(CONTACT_BOB, VER_SECOND,
+                buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+        // Change the local number to trigger update
+        getPhone(first, CONTACT_BOB, PHONE_RED).put(Phone.NUMBER, "555-1212");
+        assertDiffPattern(first,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_UPDATE),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+
+        // Merge in the second version, verify diff matches
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_UPDATE),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+    }
+
+    public void testMergeDataLocalUpdateRemoteDelete() {
+        final EntitySet first = buildSet(buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(buildBeforeEntity(CONTACT_BOB, VER_SECOND, buildPhone(PHONE_GREEN)));
+
+        // Change the local number to trigger update
+        getPhone(first, CONTACT_BOB, PHONE_RED).put(Phone.NUMBER, "555-1212");
+        assertDiffPattern(first,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_UPDATE),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+
+        // Merge in the second version, verify that our update changed to
+        // insert, since RED was deleted on remote side
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_INSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+    }
+
+    public void testMergeDataLocalDeleteRemoteUpdate() {
+        final EntitySet first = buildSet(buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(buildBeforeEntity(CONTACT_BOB, VER_SECOND, buildPhone(
+                PHONE_RED, "555-1212")));
+
+        // Delete phone locally
+        getPhone(first, CONTACT_BOB, PHONE_RED).markDeleted();
+        assertDiffPattern(first,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_DELETE),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+
+        // Merge in the second version, verify that our delete remains
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_DELETE),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+    }
+
+    public void testMergeDataLocalInsertRemoteInsert() {
+        final EntitySet first = buildSet(buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(buildBeforeEntity(CONTACT_BOB, VER_SECOND,
+                buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+        // Insert new phone locally
+        final ValuesDelta bluePhone = ValuesDelta.fromAfter(buildPhone(PHONE_BLUE));
+        first.getByRawContactId(CONTACT_BOB).addEntry(bluePhone);
+        assertDiffPattern(first,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_INSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+
+        // Merge in the second version, verify that our insert remains
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_INSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+    }
+
+    public void testMergeRawContactLocalInsertRemoteInsert() {
+        final EntitySet first = buildSet(buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(buildBeforeEntity(CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)),
+                buildBeforeEntity(CONTACT_MARY, VER_SECOND, buildPhone(PHONE_RED)));
+
+        // Add new contact locally, should remain insert
+        final EntityDelta joeContact = buildAfterEntity(buildPhone(PHONE_BLUE));
+        first.add(joeContact);
+        assertDiffPattern(first,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_INSERT),
+                buildOper(Data.CONTENT_URI, TYPE_INSERT),
+                buildOper(AggregationExceptions.CONTENT_URI, TYPE_UPDATE));
+
+        // Merge in the second version, verify that our insert remains
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_INSERT),
+                buildOper(Data.CONTENT_URI, TYPE_INSERT),
+                buildOper(AggregationExceptions.CONTENT_URI, TYPE_UPDATE));
+    }
+
+    public void testMergeRawContactLocalDeleteRemoteDelete() {
+        final EntitySet first = buildSet(
+                buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)),
+                buildBeforeEntity(CONTACT_MARY, VER_SECOND, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(
+                buildBeforeEntity(CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
+
+        // Remove contact locally
+        first.getByRawContactId(CONTACT_MARY).markDeleted();
+        assertDiffPattern(first,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_DELETE));
+
+        // Merge in the second version, verify that our delete isn't needed
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertDiffPattern(merged);
+    }
+
+    public void testMergeRawContactLocalUpdateRemoteDelete() {
+        final EntitySet first = buildSet(
+                buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)),
+                buildBeforeEntity(CONTACT_MARY, VER_SECOND, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(
+                buildBeforeEntity(CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
+
+        // Perform local update
+        getPhone(first, CONTACT_MARY, PHONE_RED).put(Phone.NUMBER, "555-1212");
+        assertDiffPattern(first,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE),
+                buildOper(Data.CONTENT_URI, TYPE_UPDATE),
+                buildOper(RawContacts.CONTENT_URI, TYPE_UPDATE));
+
+        // Merge and verify that update turned into insert
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildOper(RawContacts.CONTENT_URI, TYPE_ASSERT),
+                buildOper(RawContacts.CONTENT_URI, TYPE_INSERT),
+                buildOper(Data.CONTENT_URI, TYPE_INSERT),
+                buildOper(AggregationExceptions.CONTENT_URI, TYPE_UPDATE));
+    }
+
+    public void testMergeUsesNewVersion() {
+        final EntitySet first = buildSet(buildBeforeEntity(CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)));
+        final EntitySet second = buildSet(buildBeforeEntity(CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
+
+        assertEquals((Long)VER_FIRST, getVersion(first, CONTACT_BOB));
+        assertEquals((Long)VER_SECOND, getVersion(second, CONTACT_BOB));
+
+        final EntitySet merged = EntitySet.mergeAfter(second, first);
+        assertEquals((Long)VER_SECOND, getVersion(merged, CONTACT_BOB));
+    }
 }