Add AugmentedEntity.buildDiff() code, with unit tests.

Another step along the way to editing contacts, this change
implements building the ContentProviderOperations needed
to "diff" an existing Entity to match an edited state.

Most work is done by AugmentedValues.buildDiff(), which
builds the insert, update, or delete operation needed to
match its internal "after" state.  AugmentedEntity builds
up the list of all operations from its direct values and
any children.

When changes are made, an "enforcement" action is added to
verify that the RawContacts.VERSION matches the "before"
Entity.  If this test fails, someone else (probably a sync)
has touched the Contact, and our entire operation is rolled
back, allowing us to re-read the base Entity and retry.

This approach also handles inserting an entirely new
RawContact, which removes the need for separate create() and
save() methods in our edit UI.

Finally, two batches of unit tests have been added.  The
first batch verifies the Parcel'ing of AugmentedEntity
objects, which is used across configuration changes, and
when applying changes over a re-read Entity.  The second
batch verifies the expected ContentProviderOperations that
various buildDiff() calls should produce.
diff --git a/src/com/android/contacts/model/AugmentedEntity.java b/src/com/android/contacts/model/AugmentedEntity.java
index 5213daf..755c394 100644
--- a/src/com/android/contacts/model/AugmentedEntity.java
+++ b/src/com/android/contacts/model/AugmentedEntity.java
@@ -19,14 +19,20 @@
 import android.content.ContentProviderOperation;
 import android.content.ContentValues;
 import android.content.Entity;
+import android.content.ContentProviderOperation.Builder;
 import android.content.Entity.NamedContentValues;
+import android.net.Uri;
 import android.os.Parcel;
 import android.provider.BaseColumns;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Contains an {@link Entity} that records any modifications separately so the
@@ -43,7 +49,6 @@
  */
 public class AugmentedEntity {
     // TODO: optimize by using contentvalues pool, since we allocate so many of them
-    // TODO: write unit tests to make sure that getDiff() is performing correctly
 
     /**
      * Direct values from {@link Entity#getEntityValues()}.
@@ -60,6 +65,11 @@
         mEntries = new HashMap<String, ArrayList<AugmentedValues>>();
     }
 
+    public AugmentedEntity(AugmentedValues values) {
+        this();
+        mValues = values;
+    }
+
     /**
      * Build an {@link AugmentedEntity} using the given {@link Entity} as a
      * starting point; the "before" snapshot.
@@ -67,18 +77,23 @@
     public static AugmentedEntity fromBefore(Entity before) {
         final AugmentedEntity entity = new AugmentedEntity();
         entity.mValues = AugmentedValues.fromBefore(before.getEntityValues());
+        entity.mValues.setIdColumn(Contacts._ID);
         for (NamedContentValues namedValues : before.getSubValues()) {
             entity.addEntry(AugmentedValues.fromBefore(namedValues.values));
         }
         return entity;
     }
 
+    public AugmentedValues getValues() {
+        return mValues;
+    }
+
     /**
      * Get the {@link AugmentedValues} child marked as {@link Data#IS_PRIMARY}.
      */
     public AugmentedValues getPrimaryEntry(String mimeType) {
         // TODO: handle the case where the caller must have a non-null value,
-        // for example displayname
+        // for example inserting a displayname automatically
         final ArrayList<AugmentedValues> mimeEntries = getMimeEntries(mimeType, false);
         for (AugmentedValues entry : mimeEntries) {
             if (entry.isPrimary()) {
@@ -119,8 +134,8 @@
      * {@link BaseColumns#_ID} value, used when {@link #augmentFrom(Parcel)} is
      * inflating a modified state.
      */
-    public AugmentedValues getEntry(long anchorId) {
-        if (anchorId < 0) {
+    public AugmentedValues getEntry(Long childId) {
+        if (childId == null) {
             // Requesting an "insert" entry, which has no "before"
             return null;
         }
@@ -128,7 +143,7 @@
         // Search all children for requested entry
         for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
             for (AugmentedValues entry : mimeEntries) {
-                if (entry.getId() == anchorId) {
+                if (entry.getId() == childId) {
                     return entry;
                 }
             }
@@ -159,11 +174,11 @@
         // Read in packaged children until finished
         int mode = parcel.readInt();
         while (mode == MODE_CONTINUE) {
-            final long anchorId = parcel.readLong();
+            final Long childId = readLong(parcel);
             final ContentValues after = (ContentValues)parcel.readValue(null);
 
-            AugmentedValues entry = getEntry(anchorId);
-            if (anchorId < 0 || entry == null) {
+            AugmentedValues entry = getEntry(childId);
+            if (entry == null) {
                 // Is "insert", or "before" record is missing, so now "insert"
                 entry = AugmentedValues.fromAfter(after);
                 addEntry(entry);
@@ -185,22 +200,125 @@
         for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
             for (AugmentedValues child : mimeEntries) {
                 parcel.writeInt(MODE_CONTINUE);
-                parcel.writeLong(child.getId());
+                writeLong(parcel, child.getId());
                 parcel.writeValue(child.mAfter);
             }
         }
         parcel.writeInt(MODE_DONE);
     }
 
+    private void writeLong(Parcel parcel, Long value) {
+        parcel.writeLong(value == null ? -1 : value);
+    }
+
+    private Long readLong(Parcel parcel) {
+        final long value = parcel.readLong();
+        return value == -1 ? null : value;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (object instanceof AugmentedEntity) {
+            final AugmentedEntity other = (AugmentedEntity)object;
+
+            // Equality failed if parent values different
+            if (!other.mValues.equals(mValues)) return false;
+
+            for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
+                for (AugmentedValues child : mimeEntries) {
+                    // Equality failed if any children unmatched
+                    if (!other.containsEntry(child)) return false;
+                }
+            }
+
+            // Passed all tests, so equal
+            return true;
+        }
+        return false;
+    }
+
+    private boolean containsEntry(AugmentedValues entry) {
+        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
+            for (AugmentedValues child : mimeEntries) {
+                // Contained if we find any child that matches
+                if (child.equals(entry)) return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("\n(");
+        builder.append(mValues.toString());
+        builder.append(") = {");
+        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
+            for (AugmentedValues child : mimeEntries) {
+                builder.append("\n\t");
+                child.toString(builder);
+            }
+        }
+        builder.append("\n}\n");
+        return builder.toString();
+    }
+
+    private void possibleAdd(ArrayList<ContentProviderOperation> diff, ContentProviderOperation.Builder builder) {
+        if (builder != null) {
+            diff.add(builder.build());
+        }
+    }
+
     /**
      * Build a list of {@link ContentProviderOperation} that will transform the
      * current "before" {@link Entity} state into the modified state which this
      * {@link AugmentedEntity} represents.
      */
-    public ArrayList<ContentProviderOperation> getDiff() {
-        // TODO: assert that existing contact exists, and provide CONTACT_ID to children inserts
-        // TODO: mostly calling through to children for diff operations
-        return null;
+    public ArrayList<ContentProviderOperation> buildDiff() {
+        final ArrayList<ContentProviderOperation> diff = new ArrayList<ContentProviderOperation>();
+
+        final boolean isContactInsert = mValues.isInsert();
+        final boolean isContactDelete = mValues.isDelete();
+
+        final Long beforeId = mValues.getId();
+        final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
+
+        // Build possible operation at Contact level
+        Builder builder = mValues.buildDiff(RawContacts.CONTENT_URI);
+        possibleAdd(diff, builder);
+
+        // Build operations for all children
+        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
+            for (AugmentedValues child : mimeEntries) {
+                // Ignore children if parent was deleted
+                if (isContactDelete) continue;
+
+                builder = child.buildDiff(Data.CONTENT_URI);
+                if (child.isInsert()) {
+                    if (isContactInsert) {
+                        // Parent is brand new insert, so back-reference _id
+                        builder.withValueBackReference(Data.RAW_CONTACT_ID, 0);
+                    } else {
+                        // Inserting under existing, so fill with known _id
+                        builder.withValue(Data.RAW_CONTACT_ID, beforeId);
+                    }
+                } else if (isContactInsert) {
+                    // Child must be insert when Contact insert
+                    throw new IllegalArgumentException("When parent insert, child must be also");
+                }
+                possibleAdd(diff, builder);
+            }
+        }
+
+        // If any operations, assert that version is identical so we bail if changed
+        if (diff.size() > 0 && beforeVersion != null) {
+            builder = ContentProviderOperation.newCountQuery(Contacts.CONTENT_URI);
+            builder.withSelection(RawContacts.VERSION + "=" + beforeVersion, null);
+            builder.withExpectedCount(1);
+            possibleAdd(diff, builder);
+        }
+
+        return diff;
     }
 
     /**
@@ -211,6 +329,7 @@
     public static class AugmentedValues {
         private ContentValues mBefore;
         private ContentValues mAfter;
+        private String mIdColumn = BaseColumns._ID;
 
         private AugmentedValues() {
         }
@@ -262,17 +381,32 @@
         }
 
         public Long getId() {
-            return getAsLong(BaseColumns._ID);
+            return getAsLong(mIdColumn);
+        }
+
+        public void setIdColumn(String idColumn) {
+            mIdColumn = idColumn;
         }
 
         public boolean isPrimary() {
             return (getAsLong(Data.IS_PRIMARY) != 0);
         }
 
-        public boolean isDeleted() {
+        public boolean isDelete() {
+            // When "after" is wiped, action is "delete"
             return (mAfter == null);
         }
 
+        public boolean isUpdate() {
+            // When "after" has some changes, action is "update"
+            return (mAfter.size() > 0);
+        }
+
+        public boolean isInsert() {
+            // When no "before" id, action is "insert"
+            return mBefore == null || getId() == null;
+        }
+
         public void markDeleted() {
             mAfter = null;
         }
@@ -297,16 +431,100 @@
         }
 
         /**
+         * Return set of all keys defined through this object.
+         */
+        public Set<String> keySet() {
+            final HashSet<String> keys = new HashSet<String>();
+
+            if (mBefore != null) {
+                for (Map.Entry<String, Object> entry : mBefore.valueSet()) {
+                    keys.add(entry.getKey());
+                }
+            }
+
+            if (mAfter != null) {
+                for (Map.Entry<String, Object> entry : mAfter.valueSet()) {
+                    keys.add(entry.getKey());
+                }
+            }
+
+            return keys;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (object instanceof AugmentedValues) {
+                // Only exactly equal with both are identical subsets
+                final AugmentedValues other = (AugmentedValues)object;
+                return this.subsetEquals(other) && other.subsetEquals(this);
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder builder = new StringBuilder();
+            toString(builder);
+            return builder.toString();
+        }
+
+        /**
+         * Helper for building string representation, leveraging the given
+         * {@link StringBuilder} to minimize allocations.
+         */
+        public void toString(StringBuilder builder) {
+            builder.append("{ ");
+            for (String key : this.keySet()) {
+                builder.append(key);
+                builder.append("=");
+                builder.append(this.getAsString(key));
+                builder.append(", ");
+            }
+            builder.append("}");
+        }
+
+        /**
+         * Check if the given {@link AugmentedValues} is both a subset of this
+         * object, and any defined keys have equal values.
+         */
+        public boolean subsetEquals(AugmentedValues other) {
+            for (String key : this.keySet()) {
+                final String ourValue = this.getAsString(key);
+                final String theirValue = other.getAsString(key);
+                if (ourValue == null) {
+                    // If they have value when we're null, no match
+                    if (theirValue != null) return false;
+                } else {
+                    // If both values defined and aren't equal, no match
+                    if (!ourValue.equals(theirValue)) return false;
+                }
+            }
+            // All values compared and matched
+            return true;
+        }
+
+        /**
          * Build a {@link ContentProviderOperation} that will transform our
          * "before" state into our "after" state, using insert, update, or
          * delete as needed.
          */
-        public ContentProviderOperation getDiff() {
-            // TODO: build insert/update/delete based on internal state
-            // any _id under zero are inserts
-            return null;
+        public ContentProviderOperation.Builder buildDiff(Uri targetUri) {
+            Builder builder = null;
+            if (isInsert()) {
+                // Changed values are "insert" back-referenced to Contact
+                builder = ContentProviderOperation.newInsert(targetUri);
+                builder.withValues(mAfter);
+            } else if (isDelete()) {
+                // When marked for deletion and "before" exists, then "delete"
+                builder = ContentProviderOperation.newDelete(targetUri);
+                builder.withSelection(mIdColumn + "=" + getId(), null);
+            } else if (isUpdate()) {
+                // When has changes and "before" exists, then "update"
+                builder = ContentProviderOperation.newUpdate(targetUri);
+                builder.withSelection(mIdColumn + "=" + getId(), null);
+                builder.withValues(mAfter);
+            }
+            return builder;
         }
-
     }
-
 }
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index ba19270..1e0811e 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -19,6 +19,7 @@
 import android.database.Cursor;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.widget.EditText;
@@ -70,7 +71,7 @@
  */
 public class ContactsSource {
     /**
-     * The {@link Contacts#ACCOUNT_TYPE} these constraints apply to.
+     * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to.
      */
     public String accountType;
 
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index abb164e..dcecb95 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -113,7 +113,7 @@
         final Bundle extras = intent.getExtras();
 
         mUri = intent.getData();
-        
+
         // TODO: read all contacts part of this aggregate and hook into tabs
 
         // Resolve the intent
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index c0c7fb7..1b55c1c 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -240,7 +240,7 @@
             mEntry = entry;
             mState = state;
 
-            if (entry.isDeleted()) {
+            if (entry.isDelete()) {
                 // Hide ourselves entirely if deleted
                 mContent.setVisibility(View.GONE);
                 return;
diff --git a/tests/src/com/android/contacts/AugmentedEntityTests.java b/tests/src/com/android/contacts/AugmentedEntityTests.java
new file mode 100644
index 0000000..5e57e33
--- /dev/null
+++ b/tests/src/com/android/contacts/AugmentedEntityTests.java
@@ -0,0 +1,390 @@
+/*
+ * 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.contacts;
+
+import com.android.contacts.model.AugmentedEntity;
+import com.android.contacts.model.AugmentedEntity.AugmentedValues;
+
+import static android.content.ContentProviderOperation.TYPE_INSERT;
+import static android.content.ContentProviderOperation.TYPE_UPDATE;
+import static android.content.ContentProviderOperation.TYPE_DELETE;
+import static android.content.ContentProviderOperation.TYPE_COUNT;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.ContentProviderOperation.Builder;
+import android.os.Parcel;
+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 java.util.ArrayList;
+
+/**
+ * Tests for {@link AugmentedEntity} and {@link AugmentedValues}. These tests
+ * focus on passing changes across {@link Parcel}, and verifying that they
+ * correctly build expected "diff" operations.
+ */
+@LargeTest
+public class AugmentedEntityTests extends AndroidTestCase {
+    public static final String TAG = "AugmentedEntityTests";
+
+    private static final long TEST_CONTACT_ID = 12;
+    private static final long TEST_PHONE_ID = 24;
+
+    private static final String TEST_PHONE_NUMBER_1 = "218-555-1111";
+    private static final String TEST_PHONE_NUMBER_2 = "218-555-2222";
+
+    private static final String TEST_ACCOUNT_NAME = "TEST";
+
+    public AugmentedEntityTests() {
+        super();
+    }
+
+    @Override
+    public void setUp() {
+        mContext = getContext();
+    }
+
+    public Entity getEntity(long contactId, long phoneId) {
+        // Build an existing contact read from database
+        final ContentValues contact = new ContentValues();
+        contact.put(RawContacts.VERSION, 43);
+        contact.put(RawContacts._ID, contactId);
+
+        final ContentValues phone = new ContentValues();
+        phone.put(Data._ID, phoneId);
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+        phone.put(Phone.TYPE, Phone.TYPE_HOME);
+
+        final Entity before = new Entity(contact);
+        before.addSubValue(Data.CONTENT_URI, phone);
+        return before;
+    }
+
+    /**
+     * Test that {@link AugmentedEntity#augmentTo(Parcel)} correctly passes any
+     * changes through the {@link Parcel} object. This enforces that
+     * {@link AugmentedEntity} should be identical when serialized against the
+     * same "before" {@link Entity}.
+     */
+    public void testParcelChangesNone() {
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+
+        // No changes, should be same
+        final Parcel parcel = Parcel.obtain();
+        source.augmentTo(parcel);
+
+        final AugmentedEntity dest = AugmentedEntity.fromBefore(before);
+        parcel.setDataPosition(0);
+        dest.augmentFrom(parcel);
+
+        // Assert that we have same data rows
+        assertEquals("Changed when passing through Parcel", source, dest);
+        parcel.recycle();
+    }
+
+    public void testParcelChangesInsert() {
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+
+        // Add a new row and pass across parcel, should be same
+        final ContentValues phone = new ContentValues();
+        phone.put(Data.MIMETYPE, Phone.MIMETYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+        phone.put(Phone.TYPE, Phone.TYPE_WORK);
+        source.addEntry(AugmentedValues.fromAfter(phone));
+
+        final Parcel parcel = Parcel.obtain();
+        source.augmentTo(parcel);
+
+        final AugmentedEntity dest = AugmentedEntity.fromBefore(before);
+        parcel.setDataPosition(0);
+        dest.augmentFrom(parcel);
+
+        // Assert that we have same data rows
+        assertEquals("Changed when passing through Parcel", source, dest);
+        parcel.recycle();
+    }
+
+    public void testParcelChangesUpdate() {
+        // Update existing row and pass across parcel, should be same
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+        final AugmentedValues child = source.getEntry(TEST_PHONE_ID);
+        child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+        final Parcel parcel = Parcel.obtain();
+        source.augmentTo(parcel);
+
+        final AugmentedEntity dest = AugmentedEntity.fromBefore(before);
+        parcel.setDataPosition(0);
+        dest.augmentFrom(parcel);
+
+        // Assert that we have same data rows
+        assertEquals("Changed when passing through Parcel", source, dest);
+        parcel.recycle();
+    }
+
+    public void testParcelChangesDelete() {
+        // Delete a row and pass across parcel, should be same
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+        final AugmentedValues child = source.getEntry(TEST_PHONE_ID);
+        child.markDeleted();
+
+        final Parcel parcel = Parcel.obtain();
+        source.augmentTo(parcel);
+
+        final AugmentedEntity dest = AugmentedEntity.fromBefore(before);
+        parcel.setDataPosition(0);
+        dest.augmentFrom(parcel);
+
+        // Assert that we have same data rows
+        assertEquals("Changed when passing through Parcel", source, dest);
+        parcel.recycle();
+    }
+
+    /**
+     * Test that {@link AugmentedValues#buildDiff()} is correctly built for
+     * insert, update, and delete cases. Note this only tests behavior for
+     * individual {@link Data} rows.
+     */
+    public void testValuesDiffNone() {
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_PHONE_ID);
+        before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+
+        final AugmentedValues values = AugmentedValues.fromBefore(before);
+
+        // None action shouldn't produce a builder
+        final Builder builder = values.buildDiff(Data.CONTENT_URI);
+        assertNull("None action produced a builder", builder);
+    }
+
+    public void testValuesDiffInsert() {
+        final ContentValues after = new ContentValues();
+        after.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+        final AugmentedValues values = AugmentedValues.fromAfter(after);
+
+        // Should produce an insert action
+        final Builder builder = values.buildDiff(Data.CONTENT_URI);
+        final int type = builder.build().getType();
+        assertEquals("Didn't produce insert action", TYPE_INSERT, type);
+    }
+
+    public void testValuesDiffUpdate() {
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_PHONE_ID);
+        before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+
+        final AugmentedValues values = AugmentedValues.fromBefore(before);
+        values.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+        // Should produce an update action
+        final Builder builder = values.buildDiff(Data.CONTENT_URI);
+        final int type = builder.build().getType();
+        assertEquals("Didn't produce update action", TYPE_UPDATE, type);
+    }
+
+    public void testValuesDiffDelete() {
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_PHONE_ID);
+        before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+
+        final AugmentedValues values = AugmentedValues.fromBefore(before);
+        values.markDeleted();
+
+        // Should produce a delete action
+        final Builder builder = values.buildDiff(Data.CONTENT_URI);
+        final int type = builder.build().getType();
+        assertEquals("Didn't produce delete action", TYPE_DELETE, type);
+    }
+
+    /**
+     * Test that {@link AugmentedEntity#buildDiff()} is correctly built for
+     * insert, update, and delete cases. This only tests a subset of possible
+     * {@link Data} row changes.
+     */
+    public void testEntityDiffNone() {
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+
+        // Assert that writing unchanged produces few operations
+        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+
+        assertTrue("Created changes when none needed", (diff.size() == 0));
+    }
+
+    public void testEntityDiffNoneInsert() {
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+
+        // Insert a new phone number
+        final ContentValues phone = new ContentValues();
+        phone.put(Data.MIMETYPE, Phone.MIMETYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+        phone.put(Phone.TYPE, Phone.TYPE_WORK);
+        source.addEntry(AugmentedValues.fromAfter(phone));
+
+        // Assert two operations: insert Data row and enforce version
+        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        assertEquals("Unexpected operations", 2, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(1);
+            assertEquals("Expected version enforcement", TYPE_COUNT, oper.getType());
+        }
+    }
+
+    public void testEntityDiffUpdateInsert() {
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+
+        // Update parent contact values
+        source.getValues().put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
+
+        // Insert a new phone number
+        final ContentValues phone = new ContentValues();
+        phone.put(Data.MIMETYPE, Phone.MIMETYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+        phone.put(Phone.TYPE, Phone.TYPE_WORK);
+        source.addEntry(AugmentedValues.fromAfter(phone));
+
+        // Assert three operations: update Contact, insert Data row, enforce version
+        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(1);
+            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(2);
+            assertEquals("Expected version enforcement", TYPE_COUNT, oper.getType());
+        }
+    }
+
+    public void testEntityDiffNoneUpdate() {
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+
+        // Update existing phone number
+        final AugmentedValues child = source.getEntry(TEST_PHONE_ID);
+        child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+        // Assert two operations: update Data and enforce version
+        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        assertEquals("Unexpected operations", 2, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(1);
+            assertEquals("Expected version enforcement", TYPE_COUNT, oper.getType());
+        }
+    }
+
+    public void testEntityDiffDelete() {
+        final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
+        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+
+        // Delete entire entity
+        source.getValues().markDeleted();
+
+        // Assert two operations: delete Contact and enforce version
+        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        assertEquals("Unexpected operations", 2, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(1);
+            assertEquals("Expected version enforcement", TYPE_COUNT, oper.getType());
+        }
+    }
+
+    public void testEntityDiffInsert() {
+        // Insert a RawContact
+        final ContentValues after = new ContentValues();
+        after.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+        after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+        final AugmentedValues values = AugmentedValues.fromAfter(after);
+        final AugmentedEntity source = new AugmentedEntity(values);
+
+        // Assert two operations: delete Contact and enforce version
+        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        assertEquals("Unexpected operations", 1, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testEntityDiffInsertInsert() {
+        // Insert a RawContact
+        final ContentValues after = new ContentValues();
+        after.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+        after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+        final AugmentedValues values = AugmentedValues.fromAfter(after);
+        final AugmentedEntity source = new AugmentedEntity(values);
+
+        // Insert a new phone number
+        final ContentValues phone = new ContentValues();
+        phone.put(Data.MIMETYPE, Phone.MIMETYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+        phone.put(Phone.TYPE, Phone.TYPE_WORK);
+        source.addEntry(AugmentedValues.fromAfter(phone));
+
+        // Assert two operations: delete Contact and enforce version
+        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        assertEquals("Unexpected operations", 2, diff.size());
+        {
+            final ContentProviderOperation oper = diff.get(0);
+            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final ContentProviderOperation oper = diff.get(1);
+            assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/ContactsLaunchPerformance.java b/tests/src/com/android/contacts/ContactsLaunchPerformance.java
index 85dba56..bd60e70 100644
--- a/tests/src/com/android/contacts/ContactsLaunchPerformance.java
+++ b/tests/src/com/android/contacts/ContactsLaunchPerformance.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.tests;
+package com.android.contacts;
 
 import android.app.Activity;
 import android.test.LaunchPerformanceBase;