Merge change 9503 into eclair-dev

* changes:
  Extracts the header code from BaseContactCardActivity
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index c344711..b778f2a 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -150,6 +150,7 @@
             this.iconRes = iconRes;
             this.weight = weight;
             this.editable = editable;
+            this.typeOverallMax = -1;
         }
     }
 
@@ -169,6 +170,7 @@
         public EditType(int rawValue, int labelRes) {
             this.rawValue = rawValue;
             this.labelRes = labelRes;
+            this.specificMax = -1;
         }
 
         public EditType(int rawValue, int labelRes, boolean secondary) {
@@ -185,6 +187,20 @@
             this(rawValue, labelRes, secondary, specificMax);
             this.customColumn = customColumn;
         }
+
+        @Override
+        public boolean equals(Object object) {
+            if (object instanceof EditType) {
+                final EditType other = (EditType)object;
+                return other.rawValue == rawValue;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return rawValue;
+        }
     }
 
     /**
diff --git a/src/com/android/contacts/model/AugmentedEntity.java b/src/com/android/contacts/model/EntityDelta.java
similarity index 75%
rename from src/com/android/contacts/model/AugmentedEntity.java
rename to src/com/android/contacts/model/EntityDelta.java
index 2cded4e..09e2e98 100644
--- a/src/com/android/contacts/model/AugmentedEntity.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -34,7 +34,7 @@
 import java.util.Set;
 
 /**
- * Contains an {@link Entity} that records any modifications separately so the
+ * Contains an {@link Entity} and records any modifications separately so the
  * original {@link Entity} can be swapped out with a newer version and the
  * changes still cleanly applied.
  * <p>
@@ -46,55 +46,55 @@
  * rows are missing from the new {@link Entity}, we know the original data must
  * be deleted, but to preserve the user modifications we treat as an insert.
  */
-public class AugmentedEntity {
+public class EntityDelta {
     // TODO: optimize by using contentvalues pool, since we allocate so many of them
 
     /**
      * Direct values from {@link Entity#getEntityValues()}.
      */
-    private AugmentedValues mValues;
+    private ValuesDelta mValues;
 
     /**
      * Internal map of children values from {@link Entity#getSubValues()}, which
      * we store here sorted into {@link Data#MIMETYPE} bins.
      */
-    private HashMap<String, ArrayList<AugmentedValues>> mEntries;
+    private HashMap<String, ArrayList<ValuesDelta>> mEntries;
 
-    private AugmentedEntity() {
-        mEntries = new HashMap<String, ArrayList<AugmentedValues>>();
+    private EntityDelta() {
+        mEntries = new HashMap<String, ArrayList<ValuesDelta>>();
     }
 
-    public AugmentedEntity(AugmentedValues values) {
+    public EntityDelta(ValuesDelta values) {
         this();
         mValues = values;
     }
 
     /**
-     * Build an {@link AugmentedEntity} using the given {@link Entity} as a
+     * Build an {@link EntityDelta} using the given {@link Entity} as a
      * starting point; the "before" snapshot.
      */
-    public static AugmentedEntity fromBefore(Entity before) {
-        final AugmentedEntity entity = new AugmentedEntity();
-        entity.mValues = AugmentedValues.fromBefore(before.getEntityValues());
-        entity.mValues.setIdColumn(RawContacts._ID);
+    public static EntityDelta fromBefore(Entity before) {
+        final EntityDelta entity = new EntityDelta();
+        entity.mValues = ValuesDelta.fromBefore(before.getEntityValues());
+        entity.mValues.setIdColumn("raw_contacts", RawContacts._ID);
         for (NamedContentValues namedValues : before.getSubValues()) {
-            entity.addEntry(AugmentedValues.fromBefore(namedValues.values));
+            entity.addEntry(ValuesDelta.fromBefore(namedValues.values));
         }
         return entity;
     }
 
-    public AugmentedValues getValues() {
+    public ValuesDelta getValues() {
         return mValues;
     }
 
     /**
-     * Get the {@link AugmentedValues} child marked as {@link Data#IS_PRIMARY}.
+     * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY}.
      */
-    public AugmentedValues getPrimaryEntry(String mimeType) {
+    public ValuesDelta getPrimaryEntry(String mimeType) {
         // TODO: handle the case where the caller must have a non-null value,
         // for example inserting a displayname automatically
-        final ArrayList<AugmentedValues> mimeEntries = getMimeEntries(mimeType, false);
-        for (AugmentedValues entry : mimeEntries) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+        for (ValuesDelta entry : mimeEntries) {
             if (entry.isPrimary()) {
                 return entry;
             }
@@ -103,19 +103,19 @@
     }
 
     /**
-     * Return the list of child {@link AugmentedValues} from our optimized map,
+     * Return the list of child {@link ValuesDelta} from our optimized map,
      * creating the list if requested.
      */
-    private ArrayList<AugmentedValues> getMimeEntries(String mimeType, boolean lazyCreate) {
-        ArrayList<AugmentedValues> mimeEntries = mEntries.get(mimeType);
+    private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
+        ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
         if (mimeEntries == null && lazyCreate) {
-            mimeEntries = new ArrayList<AugmentedValues>();
+            mimeEntries = new ArrayList<ValuesDelta>();
             mEntries.put(mimeType, mimeEntries);
         }
         return mimeEntries;
     }
 
-    public ArrayList<AugmentedValues> getMimeEntries(String mimeType) {
+    public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
         return getMimeEntries(mimeType, false);
     }
 
@@ -123,25 +123,25 @@
         return mEntries.containsKey(mimeType);
     }
 
-    public void addEntry(AugmentedValues entry) {
+    public void addEntry(ValuesDelta entry) {
         final String mimeType = entry.getMimetype();
         getMimeEntries(mimeType, true).add(entry);
     }
 
     /**
-     * Find the {@link AugmentedValues} that has a specific
+     * Find the {@link ValuesDelta} that has a specific
      * {@link BaseColumns#_ID} value, used when {@link #augmentFrom(Parcel)} is
      * inflating a modified state.
      */
-    public AugmentedValues getEntry(Long childId) {
+    public ValuesDelta getEntry(Long childId) {
         if (childId == null) {
             // Requesting an "insert" entry, which has no "before"
             return null;
         }
 
         // Search all children for requested entry
-        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
-            for (AugmentedValues entry : mimeEntries) {
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta entry : mimeEntries) {
                 if (entry.getId() == childId) {
                     return entry;
                 }
@@ -150,6 +150,21 @@
         return null;
     }
 
+    /**
+     * Return the total number of {@link ValuesDelta} contained.
+     */
+    public int getEntryCount(boolean onlyVisible) {
+        int count = 0;
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                // Skip deleted items when requesting only visible
+                if (onlyVisible && child.isVisible()) continue;
+                count++;
+            }
+        }
+        return count;
+    }
+
     private static final int MODE_CONTINUE = 1;
     private static final int MODE_DONE = 2;
 
@@ -163,7 +178,7 @@
             final ContentValues after = (ContentValues)parcel.readValue(null);
             if (mValues == null) {
                 // Entity didn't exist before, so "insert"
-                mValues = AugmentedValues.fromAfter(after);
+                mValues = ValuesDelta.fromAfter(after);
             } else {
                 // Existing entity "update"
                 mValues.mAfter = after;
@@ -176,10 +191,10 @@
             final Long childId = readLong(parcel);
             final ContentValues after = (ContentValues)parcel.readValue(null);
 
-            AugmentedValues entry = getEntry(childId);
+            ValuesDelta entry = getEntry(childId);
             if (entry == null) {
                 // Is "insert", or "before" record is missing, so now "insert"
-                entry = AugmentedValues.fromAfter(after);
+                entry = ValuesDelta.fromAfter(after);
                 addEntry(entry);
             } else {
                 // Existing entry "update"
@@ -196,8 +211,8 @@
     public void augmentTo(Parcel parcel) {
         parcel.writeValue(mValues.mAfter);
 
-        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
-            for (AugmentedValues child : mimeEntries) {
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
                 parcel.writeInt(MODE_CONTINUE);
                 writeLong(parcel, child.getId());
                 parcel.writeValue(child.mAfter);
@@ -217,14 +232,14 @@
 
     @Override
     public boolean equals(Object object) {
-        if (object instanceof AugmentedEntity) {
-            final AugmentedEntity other = (AugmentedEntity)object;
+        if (object instanceof EntityDelta) {
+            final EntityDelta other = (EntityDelta)object;
 
             // Equality failed if parent values different
             if (!other.mValues.equals(mValues)) return false;
 
-            for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
-                for (AugmentedValues child : mimeEntries) {
+            for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+                for (ValuesDelta child : mimeEntries) {
                     // Equality failed if any children unmatched
                     if (!other.containsEntry(child)) return false;
                 }
@@ -236,9 +251,9 @@
         return false;
     }
 
-    private boolean containsEntry(AugmentedValues entry) {
-        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
-            for (AugmentedValues child : mimeEntries) {
+    private boolean containsEntry(ValuesDelta entry) {
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
                 // Contained if we find any child that matches
                 if (child.equals(entry)) return true;
             }
@@ -252,8 +267,8 @@
         builder.append("\n(");
         builder.append(mValues.toString());
         builder.append(") = {");
-        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
-            for (AugmentedValues child : mimeEntries) {
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
                 builder.append("\n\t");
                 child.toString(builder);
             }
@@ -271,7 +286,7 @@
     /**
      * Build a list of {@link ContentProviderOperation} that will transform the
      * current "before" {@link Entity} state into the modified state which this
-     * {@link AugmentedEntity} represents.
+     * {@link EntityDelta} represents.
      */
     public ArrayList<ContentProviderOperation> buildDiff() {
         final ArrayList<ContentProviderOperation> diff = new ArrayList<ContentProviderOperation>();
@@ -287,8 +302,8 @@
         possibleAdd(diff, builder);
 
         // Build operations for all children
-        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
-            for (AugmentedValues child : mimeEntries) {
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
                 // Ignore children if parent was deleted
                 if (isContactDelete) continue;
 
@@ -327,31 +342,32 @@
      * modified version of that state. This allows us to build insert, update,
      * or delete operations based on a "before" {@link Entity} snapshot.
      */
-    public static class AugmentedValues {
+    public static class ValuesDelta {
         private ContentValues mBefore;
         private ContentValues mAfter;
+        private String mIdTable = "data";
         private String mIdColumn = BaseColumns._ID;
 
-        private AugmentedValues() {
+        private ValuesDelta() {
         }
 
         /**
-         * Create {@link AugmentedValues}, using the given object as the
+         * Create {@link ValuesDelta}, using the given object as the
          * "before" state, usually from an {@link Entity}.
          */
-        public static AugmentedValues fromBefore(ContentValues before) {
-            final AugmentedValues entry = new AugmentedValues();
+        public static ValuesDelta fromBefore(ContentValues before) {
+            final ValuesDelta entry = new ValuesDelta();
             entry.mBefore = before;
             entry.mAfter = new ContentValues();
             return entry;
         }
 
         /**
-         * Create {@link AugmentedValues}, using the given object as the "after"
+         * Create {@link ValuesDelta}, using the given object as the "after"
          * state, usually when we are inserting a row instead of updating.
          */
-        public static AugmentedValues fromAfter(ContentValues after) {
-            final AugmentedValues entry = new AugmentedValues();
+        public static ValuesDelta fromAfter(ContentValues after) {
+            final ValuesDelta entry = new ValuesDelta();
             entry.mBefore = null;
             entry.mAfter = after;
             return entry;
@@ -385,7 +401,9 @@
             return getAsLong(mIdColumn);
         }
 
-        public void setIdColumn(String idColumn) {
+        public void setIdColumn(String idTable, String idColumn) {
+            // TODO: remove idTable when we've fixed contentprovider
+            mIdTable = idTable;
             mIdColumn = idColumn;
         }
 
@@ -393,19 +411,28 @@
             return (getAsLong(Data.IS_PRIMARY) != 0);
         }
 
+        public boolean beforeExists() {
+            return (mBefore != null && mBefore.containsKey(mIdColumn));
+        }
+
+        public boolean isVisible() {
+            // When "after" is present, then visible
+            return (mAfter != null);
+        }
+
         public boolean isDelete() {
             // When "after" is wiped, action is "delete"
-            return (mAfter == null);
+            return beforeExists() && (mAfter == null);
         }
 
         public boolean isUpdate() {
             // When "after" has some changes, action is "update"
-            return (mAfter.size() > 0);
+            return beforeExists() && (mAfter.size() > 0);
         }
 
         public boolean isInsert() {
-            // When no "before" id, action is "insert"
-            return mBefore == null || getId() == null;
+            // When no "before" id, and has "after", action is "insert"
+            return !beforeExists() && (mAfter != null);
         }
 
         public void markDeleted() {
@@ -454,9 +481,9 @@
 
         @Override
         public boolean equals(Object object) {
-            if (object instanceof AugmentedValues) {
+            if (object instanceof ValuesDelta) {
                 // Only exactly equal with both are identical subsets
-                final AugmentedValues other = (AugmentedValues)object;
+                final ValuesDelta other = (ValuesDelta)object;
                 return this.subsetEquals(other) && other.subsetEquals(this);
             }
             return false;
@@ -485,10 +512,10 @@
         }
 
         /**
-         * Check if the given {@link AugmentedValues} is both a subset of this
+         * Check if the given {@link ValuesDelta} is both a subset of this
          * object, and any defined keys have equal values.
          */
-        public boolean subsetEquals(AugmentedValues other) {
+        public boolean subsetEquals(ValuesDelta other) {
             for (String key : this.keySet()) {
                 final String ourValue = this.getAsString(key);
                 final String theirValue = other.getAsString(key);
@@ -518,11 +545,11 @@
             } else if (isDelete()) {
                 // When marked for deletion and "before" exists, then "delete"
                 builder = ContentProviderOperation.newDelete(targetUri);
-                builder.withSelection(mIdColumn + "=" + getId(), null);
+                builder.withSelection(mIdTable + "." + mIdColumn + "=" + getId(), null);
             } else if (isUpdate()) {
                 // When has changes and "before" exists, then "update"
                 builder = ContentProviderOperation.newUpdate(targetUri);
-                builder.withSelection(mIdColumn + "=" + getId(), null);
+                builder.withSelection(mIdTable + "." + mIdColumn + "=" + getId(), null);
                 builder.withValues(mAfter);
             }
             return builder;
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index e68d0dc..5dd2880 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -16,41 +16,134 @@
 
 package com.android.contacts.model;
 
-import com.android.contacts.model.AugmentedEntity.AugmentedValues;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.model.ContactsSource.EditType;
 
 import android.content.ContentValues;
 import android.provider.ContactsContract.Data;
+import android.util.SparseIntArray;
 
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 
 /**
- * Helper methods for modifying an {@link AugmentedEntity}, such as inserting
+ * Helper methods for modifying an {@link EntityDelta}, such as inserting
  * new rows, or enforcing {@link ContactsSource}.
  */
 public class EntityModifier {
-    // TODO: provide helper to force an augmentedentity into sourceconstraints?
-
     /**
-     * For the given {@link AugmentedEntity}, determine if the given
+     * For the given {@link EntityDelta}, determine if the given
      * {@link DataKind} could be inserted under specific
      * {@link ContactsSource}.
      */
-    public static boolean canInsert(AugmentedEntity contact, DataKind kind) {
-        // TODO: compare against constraints to determine if insert is possible
-        return true;
+    public static boolean canInsert(EntityDelta state, DataKind kind) {
+        // Insert possible when have valid types and under overall maximum
+        final boolean validTypes = hasValidTypes(state, kind);
+        final boolean validOverall = (kind.typeOverallMax == -1)
+                || (state.getEntryCount(true) < kind.typeOverallMax);
+        return (validTypes && validOverall);
+    }
+
+    public static boolean hasValidTypes(EntityDelta state, DataKind kind) {
+        return (getValidTypes(state, kind).size() > 0);
     }
 
     /**
-     * For the given {@link AugmentedEntity} and {@link DataKind}, return the
+     * For the given {@link EntityDelta} and {@link DataKind}, return the
      * list possible {@link EditType} options available based on
      * {@link ContactsSource}.
      */
-    public static List<EditType> getValidTypes(AugmentedEntity entity, DataKind kind,
+    public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind) {
+        return getValidTypes(state, kind, null, true, null);
+    }
+
+    /**
+     * For the given {@link EntityDelta} and {@link DataKind}, return the
+     * list possible {@link EditType} options available based on
+     * {@link ContactsSource}.
+     *
+     * @param forceInclude Always include this {@link EditType} in the returned
+     *            list, even when an otherwise-invalid choice. This is useful
+     *            when showing a dialog that includes the current type.
+     */
+    public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind,
             EditType forceInclude) {
-        // TODO: enforce constraints and include any extra provided
-        return kind.typeList;
+        return getValidTypes(state, kind, forceInclude, true, null);
+    }
+
+    /**
+     * For the given {@link EntityDelta} and {@link DataKind}, return the
+     * list possible {@link EditType} options available based on
+     * {@link ContactsSource}.
+     *
+     * @param forceInclude Always include this {@link EditType} in the returned
+     *            list, even when an otherwise-invalid choice. This is useful
+     *            when showing a dialog that includes the current type.
+     * @param includeSecondary If true, include any valid types marked as
+     *            {@link EditType#secondary}.
+     * @param typeCount When provided, will be used for the frequency count of
+     *            each {@link EditType}, otherwise built using
+     *            {@link #getTypeFrequencies(EntityDelta, DataKind)}.
+     */
+    private static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind,
+            EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) {
+        final ArrayList<EditType> validTypes = new ArrayList<EditType>();
+
+        // Bail early if no types provided
+        if (!hasEditTypes(kind)) return validTypes;
+
+        if (typeCount == null) {
+            // Build frequency counts if not provided
+            typeCount = getTypeFrequencies(state, kind);
+        }
+
+        // Build list of valid types
+        final int overallCount = typeCount.get(FREQUENCY_TOTAL);
+        for (EditType type : kind.typeList) {
+            final boolean validUnlimited = (type.specificMax == -1 && overallCount < kind.typeOverallMax);
+            final boolean validSpecific = (typeCount.get(type.rawValue) < type.specificMax);
+            final boolean validSecondary = (includeSecondary ? true : !type.secondary);
+            final boolean forcedInclude = type.equals(forceInclude);
+            if (forcedInclude || (validSecondary && (validUnlimited || validSpecific))) {
+                // Type is valid when no limit, under limit, or forced include
+                validTypes.add(type);
+            }
+        }
+
+        return validTypes;
+    }
+
+    private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE;
+
+    /**
+     * Count up the frequency that each {@link EditType} appears in the given
+     * {@link EntityDelta}. The returned {@link SparseIntArray} maps from
+     * {@link EditType#rawValue} to counts, with the total overall count stored
+     * as {@link #FREQUENCY_TOTAL}.
+     */
+    private static SparseIntArray getTypeFrequencies(EntityDelta state, DataKind kind) {
+        final SparseIntArray typeCount = new SparseIntArray();
+
+        // Find all entries for this kind, bailing early if none found
+        final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType);
+        if (mimeEntries == null) return typeCount;
+
+        int totalCount = 0;
+        for (ValuesDelta entry : mimeEntries) {
+            // Only count visible entries
+            if (!entry.isVisible()) continue;
+            totalCount++;
+
+            final EditType type = getCurrentType(entry, kind);
+            if (type != null) {
+                final int count = typeCount.get(type.rawValue);
+                typeCount.put(type.rawValue, count + 1);
+            }
+        }
+        typeCount.put(FREQUENCY_TOTAL, totalCount);
+        return typeCount;
     }
 
     /**
@@ -63,11 +156,19 @@
 
     /**
      * Find the {@link EditType} that describes the given
-     * {@link AugmentedValues} row, assuming the given {@link DataKind} dictates
+     * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates
      * the possible types.
      */
-    public static EditType getCurrentType(AugmentedValues entry, DataKind kind) {
-        final long rawValue = entry.getAsLong(kind.typeColumn);
+    public static EditType getCurrentType(ValuesDelta entry, DataKind kind) {
+        final Long rawValue = entry.getAsLong(kind.typeColumn);
+        if (rawValue == null) return null;
+        return getType(kind, rawValue.intValue());
+    }
+
+    /**
+     * Find the {@link EditType} with the given {@link EditType#rawValue}.
+     */
+    public static EditType getType(DataKind kind, int rawValue) {
         for (EditType type : kind.typeList) {
             if (type.rawValue == rawValue) {
                 return type;
@@ -77,11 +178,64 @@
     }
 
     /**
-     * Insert a new child of kind {@link DataKind} into the given
-     * {@link AugmentedEntity}. Assumes the caller has already checked
-     * {@link #canInsert(AugmentedEntity, DataKind)}.
+     * Find the best {@link EditType} for a potential insert. The "best" is the
+     * first primary type that doesn't already exist. When all valid types
+     * exist, we pick the last valid option.
      */
-    public static void insertChild(AugmentedEntity state, DataKind kind) {
+    public static EditType getBestValidType(EntityDelta state, DataKind kind,
+            boolean includeSecondary) {
+        // Shortcut when no types
+        if (kind.typeColumn == null) return null;
+
+        // Find type counts and valid primary types, bail if none
+        final SparseIntArray typeCount = getTypeFrequencies(state, kind);
+        final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary,
+                typeCount);
+        if (validTypes.size() == 0) return null;
+
+        // Keep track of the last valid type
+        final EditType lastType = validTypes.get(validTypes.size() - 1);
+
+        // Remove any types that already exist
+        Iterator<EditType> iterator = validTypes.iterator();
+        while (iterator.hasNext()) {
+            final EditType type = iterator.next();
+            final int count = typeCount.get(type.rawValue);
+
+            if (count > 0) {
+                // Type already appears, so don't consider
+                iterator.remove();
+            }
+        }
+
+        // Use the best remaining, otherwise the last valid
+        if (validTypes.size() > 0) {
+            return validTypes.get(0);
+        } else {
+            return lastType;
+        }
+    }
+
+    /**
+     * Insert a new child of kind {@link DataKind} into the given
+     * {@link EntityDelta}. Tries using the best {@link EditType} found
+     * using {@link #getBestValidType(EntityDelta, DataKind, boolean)}.
+     */
+    public static void insertChild(EntityDelta state, DataKind kind) {
+        // First try finding a valid primary
+        EditType bestType = getBestValidType(state, kind, false);
+        if (bestType == null) {
+            // No valid primary found, so expand search to secondary
+            bestType = getBestValidType(state, kind, true);
+        }
+        insertChild(state, kind, bestType);
+    }
+
+    /**
+     * Insert a new child of kind {@link DataKind} into the given
+     * {@link EntityDelta}, marked with the given {@link EditType}.
+     */
+    public static void insertChild(EntityDelta state, DataKind kind, EditType type) {
         final ContentValues after = new ContentValues();
 
         // Our parent CONTACT_ID is provided later
@@ -92,13 +246,12 @@
             after.putAll(kind.defaultValues);
         }
 
-        if (kind.typeColumn != null) {
-            // TODO: add the best-kind of entry based on current state machine
-            final EditType firstType = kind.typeList.get(0);
-            after.put(kind.typeColumn, firstType.rawValue);
+        if (kind.typeColumn != null && type != null) {
+            // Set type, if provided
+            after.put(kind.typeColumn, type.rawValue);
         }
 
-        state.addEntry(AugmentedValues.fromAfter(after));
+        state.addEntry(ValuesDelta.fromAfter(after));
     }
 
 }
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 7555b20..94a67bf 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -17,7 +17,7 @@
 package com.android.contacts.ui;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AugmentedEntity;
+import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.ui.widget.ContactEditorView;
@@ -78,7 +78,7 @@
 //    private boolean mContactChanged = false;
 
     private Uri mUri;
-    private ArrayList<AugmentedEntity> mEntities = new ArrayList<AugmentedEntity>();
+    private ArrayList<EntityDelta> mEntities = new ArrayList<EntityDelta>();
 
     private ContentResolver mResolver;
     private ContactEditorView mEditor;
@@ -128,7 +128,7 @@
                         ContactsContract.RawContacts.CONTACT_ID + "=" + aggId, null, null);
                 while (iterator.hasNext()) {
                     final Entity before = iterator.next();
-                    final AugmentedEntity entity = AugmentedEntity.fromBefore(before);
+                    final EntityDelta entity = EntityDelta.fromBefore(before);
 
                     mEntities.add(entity);
 
@@ -555,7 +555,7 @@
 
         final ContentResolver resolver = this.getContentResolver();
 
-        for (AugmentedEntity entity : mEntities) {
+        for (EntityDelta entity : mEntities) {
 
             Log.d(TAG, "about to persist " + entity.toString());
 
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 924241f..5e7785a 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -17,10 +17,10 @@
 package com.android.contacts.ui.widget;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AugmentedEntity;
+import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.AugmentedEntity.AugmentedValues;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
@@ -50,11 +50,11 @@
 
 /**
  * Custom view that provides all the editor interaction for a specific
- * {@link Contacts} represented through an {@link AugmentedEntity}. Callers can
+ * {@link Contacts} represented through an {@link EntityDelta}. Callers can
  * reuse this view and quickly rebuild its contents through
- * {@link #setState(AugmentedEntity, ContactsSource)}.
+ * {@link #setState(EntityDelta, ContactsSource)}.
  * <p>
- * Internal updates are performed against {@link AugmentedValues} so that the
+ * Internal updates are performed against {@link ValuesDelta} so that the
  * source {@link Entity} can be swapped out. Any state-based changes, such as
  * adding {@link Data} rows or changing {@link EditType}, are performed through
  * {@link EntityModifier} to ensure that {@link ContactsSource} are enforced.
@@ -80,10 +80,10 @@
 
     /**
      * Set the internal state for this view, given a current
-     * {@link AugmentedEntity} state and the {@link ContactsSource} that
+     * {@link EntityDelta} state and the {@link ContactsSource} that
      * apply to that state.
      */
-    public void setState(AugmentedEntity state, ContactsSource source) {
+    public void setState(EntityDelta state, ContactsSource source) {
         // Remove any existing sections
         mGeneral.removeAllViews();
         mSecondary.removeAllViews();
@@ -96,11 +96,11 @@
             final String mimeType = kind.mimeType;
             if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 // Handle special case editor for structured name
-                final AugmentedValues primary = state.getPrimaryEntry(mimeType);
+                final ValuesDelta primary = state.getPrimaryEntry(mimeType);
                 mDisplayName.setValues(null, primary, state);
             } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 // Handle special case editor for photos
-                final AugmentedValues firstValue = state.getPrimaryEntry(mimeType);
+                final ValuesDelta firstValue = state.getPrimaryEntry(mimeType);
                 mPhoto.setValues(null, firstValue, state);
             } else {
                 // Otherwise use generic section-based editors
@@ -120,7 +120,7 @@
      * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
      * section header and a trigger for adding new {@link Data} rows.
      */
-    public static class KindSection extends ViewHolder implements OnClickListener {
+    protected static class KindSection extends ViewHolder implements OnClickListener, EditorListener {
         private static final int RES_SECTION = R.layout.item_edit_kind;
 
         private ViewGroup mEditors;
@@ -128,9 +128,9 @@
         private TextView mTitle;
 
         private DataKind mKind;
-        private AugmentedEntity mState;
+        private EntityDelta mState;
 
-        public KindSection(Context context, DataKind kind, AugmentedEntity state) {
+        public KindSection(Context context, DataKind kind, EntityDelta state) {
             super(context, RES_SECTION);
 
             mKind = kind;
@@ -144,7 +144,12 @@
             mTitle = (TextView)mContent.findViewById(R.id.kind_title);
             mTitle.setText(kind.titleRes);
 
-            rebuildFromState();
+            this.rebuildFromState();
+            this.updateAddEnabled();
+        }
+
+        public void onDeleted(Editor editor) {
+            this.updateAddEnabled();
         }
 
         /**
@@ -152,46 +157,71 @@
          */
         public void rebuildFromState() {
             // TODO: build special "stub" entries to help enter first-phone or first-email
-            // TODO: set the add-enabled state based on entitymodifier
 
             // Remove any existing editors
             mEditors.removeAllViews();
 
             // Build individual editors for each entry
             if (!mState.hasMimeEntries(mKind.mimeType)) return;
-            for (AugmentedValues entry : mState.getMimeEntries(mKind.mimeType)) {
+            for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
+                // Skip entries that aren't visible
+                if (!entry.isVisible()) continue;
+
                 final GenericEditor editor = new GenericEditor(mContext);
                 editor.setValues(mKind, entry, mState);
+                editor.setEditorListener(this);
                 mEditors.addView(editor.getView());
             }
         }
 
+        protected void updateAddEnabled() {
+            // Set enabled state on the "add" view
+            final boolean canInsert = EntityModifier.canInsert(mState, mKind);
+            mAdd.setEnabled(canInsert);
+        }
+
         public void onClick(View v) {
             // Insert a new child and rebuild
             EntityModifier.insertChild(mState, mKind);
-            rebuildFromState();
+            this.rebuildFromState();
+            this.updateAddEnabled();
         }
     }
 
     /**
      * Generic definition of something that edits a {@link Data} row through an
-     * {@link AugmentedValues} object.
+     * {@link ValuesDelta} object.
      */
-    public interface Editor {
+    protected interface Editor {
         /**
-         * Prepare this editor for the given {@link AugmentedValues}, which
+         * Prepare this editor for the given {@link ValuesDelta}, which
          * builds any needed views. Any changes performed by the user will be
          * written back to that same object.
          */
-        public void setValues(DataKind kind, AugmentedValues values, AugmentedEntity state);
+        public void setValues(DataKind kind, ValuesDelta values, EntityDelta state);
+
+        /**
+         * Add a specific {@link EditorListener} to this {@link Editor}.
+         */
+        public void setEditorListener(EditorListener listener);
+    }
+
+    /**
+     * Listener for an {@link Editor}, usually to handle deleted items.
+     */
+    protected interface EditorListener {
+        /**
+         * Called when the given {@link Editor} has been deleted.
+         */
+        public void onDeleted(Editor editor);
     }
 
     /**
      * Simple editor that handles labels and any {@link EditField} defined for
-     * the entry. Uses {@link AugmentedValues} to read any existing
+     * the entry. Uses {@link ValuesDelta} to read any existing
      * {@link Entity} values, and to correctly write any changes values.
      */
-    public static class GenericEditor extends ViewHolder implements Editor, OnClickListener {
+    protected static class GenericEditor extends ViewHolder implements Editor, OnClickListener {
         private static final int RES_EDITOR = R.layout.item_editor;
         private static final int RES_FIELD = R.layout.item_editor_field;
         private static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
@@ -201,8 +231,8 @@
         private View mDelete;
 
         private DataKind mKind;
-        private AugmentedValues mEntry;
-        private AugmentedEntity mState;
+        private ValuesDelta mEntry;
+        private EntityDelta mState;
 
         private EditType mType;
 
@@ -218,6 +248,12 @@
             mDelete.setOnClickListener(this);
         }
 
+        private EditorListener mListener;
+
+        public void setEditorListener(EditorListener listener) {
+            mListener = listener;
+        }
+
         /**
          * Build the current label state based on selected {@link EditType} and
          * possible custom label string.
@@ -242,12 +278,12 @@
             mLabel.setText(mType.labelRes);
         }
 
-        public void setValues(DataKind kind, AugmentedValues entry, AugmentedEntity state) {
+        public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state) {
             mKind = kind;
             mEntry = entry;
             mState = state;
 
-            if (entry.isDelete()) {
+            if (!entry.isVisible()) {
                 // Hide ourselves entirely if deleted
                 mContent.setVisibility(View.GONE);
                 return;
@@ -386,6 +422,11 @@
                     // Mark as deleted and hide this editor
                     mEntry.markDeleted();
                     mContent.setVisibility(View.GONE);
+
+                    if (mListener != null) {
+                        // Notify listener when present
+                        mListener.onDeleted(this);
+                    }
                     break;
                 }
             }
@@ -395,7 +436,7 @@
     /**
      * Simple editor for {@link Photo}.
      */
-    public static class PhotoEditor extends ViewHolder implements Editor {
+    protected static class PhotoEditor extends ViewHolder implements Editor {
         private static final int RES_PHOTO = R.layout.item_editor_photo;
 
         public PhotoEditor(Context context) {
@@ -452,21 +493,27 @@
 //            setPhotoPresent(false);
 //        }
 
-        public void setValues(DataKind kind, AugmentedValues values, AugmentedEntity state) {
+        public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
+        }
+
+        public void setEditorListener(EditorListener listener) {
         }
     }
 
     /**
      * Simple editor for {@link StructuredName}.
      */
-    public static class DisplayNameEditor extends ViewHolder implements Editor {
+    protected static class DisplayNameEditor extends ViewHolder implements Editor {
         private static final int RES_DISPLAY_NAME = R.layout.item_editor_displayname;
 
         public DisplayNameEditor(Context context) {
             super(context, RES_DISPLAY_NAME);
         }
 
-        public void setValues(DataKind kind, AugmentedValues values, AugmentedEntity state) {
+        public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
+        }
+
+        public void setEditorListener(EditorListener listener) {
         }
     }
 
diff --git a/tests/src/com/android/contacts/AugmentedEntityTests.java b/tests/src/com/android/contacts/EntityDeltaTests.java
similarity index 75%
rename from tests/src/com/android/contacts/AugmentedEntityTests.java
rename to tests/src/com/android/contacts/EntityDeltaTests.java
index e9498e1..791f311 100644
--- a/tests/src/com/android/contacts/AugmentedEntityTests.java
+++ b/tests/src/com/android/contacts/EntityDeltaTests.java
@@ -16,8 +16,8 @@
 
 package com.android.contacts;
 
-import com.android.contacts.model.AugmentedEntity;
-import com.android.contacts.model.AugmentedEntity.AugmentedValues;
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
 
 import static android.content.ContentProviderOperation.TYPE_INSERT;
 import static android.content.ContentProviderOperation.TYPE_UPDATE;
@@ -38,12 +38,12 @@
 import java.util.ArrayList;
 
 /**
- * Tests for {@link AugmentedEntity} and {@link AugmentedValues}. These tests
+ * Tests for {@link EntityDelta} and {@link ValuesDelta}. 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 class EntityDeltaTests extends AndroidTestCase {
     public static final String TAG = "AugmentedEntityTests";
 
     private static final long TEST_CONTACT_ID = 12;
@@ -54,7 +54,7 @@
 
     private static final String TEST_ACCOUNT_NAME = "TEST";
 
-    public AugmentedEntityTests() {
+    public EntityDeltaTests() {
         super();
     }
 
@@ -63,7 +63,7 @@
         mContext = getContext();
     }
 
-    public Entity getEntity(long contactId, long phoneId) {
+    protected Entity getEntity(long contactId, long phoneId) {
         // Build an existing contact read from database
         final ContentValues contact = new ContentValues();
         contact.put(RawContacts.VERSION, 43);
@@ -81,91 +81,68 @@
     }
 
     /**
-     * Test that {@link AugmentedEntity#augmentTo(Parcel)} correctly passes any
+     * Test that {@link EntityDelta#augmentTo(Parcel)} correctly passes any
      * changes through the {@link Parcel} object. This enforces that
-     * {@link AugmentedEntity} should be identical when serialized against the
+     * {@link EntityDelta} 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);
+        final EntityDelta source = EntityDelta.fromBefore(before);
+        final EntityDelta dest = EntityDelta.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();
+        // Merge modified values and assert they match
+        dest.mergeAfter(source);
+        assertEquals("Unexpected change when merging", source, dest);
     }
 
     public void testParcelChangesInsert() {
         final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
-        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+        final EntityDelta source = EntityDelta.fromBefore(before);
+        final EntityDelta dest = EntityDelta.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));
+        source.addEntry(ValuesDelta.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();
+        // Merge modified values and assert they match
+        dest.mergeAfter(source);
+        assertEquals("Unexpected change when merging", source, dest);
     }
 
     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);
+        final EntityDelta source = EntityDelta.fromBefore(before);
+        final EntityDelta dest = EntityDelta.fromBefore(before);
+
+        final ValuesDelta 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();
+        // Merge modified values and assert they match
+        dest.mergeAfter(source);
+        assertEquals("Unexpected change when merging", source, dest);
     }
 
     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);
+        final EntityDelta source = EntityDelta.fromBefore(before);
+        final EntityDelta dest = EntityDelta.fromBefore(before);
+
+        final ValuesDelta 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();
+        // Merge modified values and assert they match
+        dest.mergeAfter(source);
+        assertEquals("Unexpected change when merging", source, dest);
     }
 
     /**
-     * Test that {@link AugmentedValues#buildDiff()} is correctly built for
+     * Test that {@link ValuesDelta#buildDiff()} is correctly built for
      * insert, update, and delete cases. Note this only tests behavior for
      * individual {@link Data} rows.
      */
@@ -174,7 +151,7 @@
         before.put(Data._ID, TEST_PHONE_ID);
         before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
 
-        final AugmentedValues values = AugmentedValues.fromBefore(before);
+        final ValuesDelta values = ValuesDelta.fromBefore(before);
 
         // None action shouldn't produce a builder
         final Builder builder = values.buildDiff(Data.CONTENT_URI);
@@ -185,7 +162,7 @@
         final ContentValues after = new ContentValues();
         after.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
 
-        final AugmentedValues values = AugmentedValues.fromAfter(after);
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
 
         // Should produce an insert action
         final Builder builder = values.buildDiff(Data.CONTENT_URI);
@@ -198,7 +175,7 @@
         before.put(Data._ID, TEST_PHONE_ID);
         before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
 
-        final AugmentedValues values = AugmentedValues.fromBefore(before);
+        final ValuesDelta values = ValuesDelta.fromBefore(before);
         values.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
 
         // Should produce an update action
@@ -212,7 +189,7 @@
         before.put(Data._ID, TEST_PHONE_ID);
         before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
 
-        final AugmentedValues values = AugmentedValues.fromBefore(before);
+        final ValuesDelta values = ValuesDelta.fromBefore(before);
         values.markDeleted();
 
         // Should produce a delete action
@@ -222,13 +199,13 @@
     }
 
     /**
-     * Test that {@link AugmentedEntity#buildDiff()} is correctly built for
+     * Test that {@link EntityDelta#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);
+        final EntityDelta source = EntityDelta.fromBefore(before);
 
         // Assert that writing unchanged produces few operations
         final ArrayList<ContentProviderOperation> diff = source.buildDiff();
@@ -238,14 +215,14 @@
 
     public void testEntityDiffNoneInsert() {
         final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
-        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+        final EntityDelta source = EntityDelta.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));
+        source.addEntry(ValuesDelta.fromAfter(phone));
 
         // Assert two operations: insert Data row and enforce version
         final ArrayList<ContentProviderOperation> diff = source.buildDiff();
@@ -263,7 +240,7 @@
 
     public void testEntityDiffUpdateInsert() {
         final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
-        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+        final EntityDelta source = EntityDelta.fromBefore(before);
 
         // Update parent contact values
         source.getValues().put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
@@ -273,7 +250,7 @@
         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));
+        source.addEntry(ValuesDelta.fromAfter(phone));
 
         // Assert three operations: update Contact, insert Data row, enforce version
         final ArrayList<ContentProviderOperation> diff = source.buildDiff();
@@ -296,10 +273,10 @@
 
     public void testEntityDiffNoneUpdate() {
         final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
-        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+        final EntityDelta source = EntityDelta.fromBefore(before);
 
         // Update existing phone number
-        final AugmentedValues child = source.getEntry(TEST_PHONE_ID);
+        final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
         child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
 
         // Assert two operations: update Data and enforce version
@@ -318,7 +295,7 @@
 
     public void testEntityDiffDelete() {
         final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
-        final AugmentedEntity source = AugmentedEntity.fromBefore(before);
+        final EntityDelta source = EntityDelta.fromBefore(before);
 
         // Delete entire entity
         source.getValues().markDeleted();
@@ -343,8 +320,8 @@
         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);
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+        final EntityDelta source = new EntityDelta(values);
 
         // Assert two operations: delete Contact and enforce version
         final ArrayList<ContentProviderOperation> diff = source.buildDiff();
@@ -362,15 +339,15 @@
         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);
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+        final EntityDelta source = new EntityDelta(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));
+        source.addEntry(ValuesDelta.fromAfter(phone));
 
         // Assert two operations: delete Contact and enforce version
         final ArrayList<ContentProviderOperation> diff = source.buildDiff();
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
new file mode 100644
index 0000000..9d41f67
--- /dev/null
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -0,0 +1,213 @@
+/*
+ * 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.EntityDelta;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditType;
+
+import android.content.ContentValues;
+import android.content.Entity;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link EntityModifier} to verify that {@link ContactsSource}
+ * constraints are being enforced correctly.
+ */
+@LargeTest
+public class EntityModifierTests extends AndroidTestCase {
+    public static final String TAG = "EntityModifierTests";
+
+    public EntityModifierTests() {
+        super();
+    }
+
+    @Override
+    public void setUp() {
+        mContext = getContext();
+    }
+
+    /**
+     * Build a {@link ContactsSource} that has various odd constraints for
+     * testing purposes.
+     */
+    protected ContactsSource getSource() {
+        final ContactsSource list = new ContactsSource();
+
+        {
+            // Phone allows maximum 2 home, 1 work, and unlimited other, with
+            // constraint of 5 numbers maximum.
+            DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE, -1, -1, 10, true);
+
+            kind.typeOverallMax = 5;
+            kind.typeColumn = Phone.TYPE;
+            kind.typeList = new ArrayList<EditType>();
+            kind.typeList.add(new EditType(Phone.TYPE_HOME, -1, false, 2));
+            kind.typeList.add(new EditType(Phone.TYPE_WORK, -1, false, 1));
+            kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1, true, -1));
+            kind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));
+
+            list.add(kind);
+        }
+
+        return list;
+    }
+
+    /**
+     * Build an {@link Entity} with the requested set of phone numbers.
+     */
+    protected EntityDelta getEntity() {
+        final ContentValues contact = new ContentValues();
+        final Entity before = new Entity(contact);
+        return EntityDelta.fromBefore(before);
+    }
+
+    /**
+     * Assert this {@link List} contains the given {@link Object}.
+     */
+    protected void assertContains(List<?> list, Object object) {
+        assertTrue("Missing expected value", list.contains(object));
+    }
+
+    /**
+     * Assert this {@link List} does not contain the given {@link Object}.
+     */
+    protected void assertNotContains(List<?> list, Object object) {
+        assertFalse("Contained unexpected value", list.contains(object));
+    }
+
+    /**
+     * Insert various rows to test
+     * {@link EntityModifier#getValidTypes(EntityDelta, DataKind, EditType)}
+     */
+    public void testValidTypes() {
+        // Build a source and pull specific types
+        final ContactsSource source = getSource();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+        final EditType typeWork = EntityModifier.getType(kindPhone, Phone.TYPE_WORK);
+        final EditType typeOther = EntityModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+        List<EditType> validTypes;
+
+        // Add first home, first work
+        final EntityDelta state = getEntity();
+        EntityModifier.insertChild(state, kindPhone, typeHome);
+        EntityModifier.insertChild(state, kindPhone, typeWork);
+
+        // Expecting home, other
+        validTypes = EntityModifier.getValidTypes(state, kindPhone, null);
+        assertContains(validTypes, typeHome);
+        assertNotContains(validTypes, typeWork);
+        assertContains(validTypes, typeOther);
+
+        // Add second home
+        EntityModifier.insertChild(state, kindPhone, typeHome);
+
+        // Expecting other
+        validTypes = EntityModifier.getValidTypes(state, kindPhone, null);
+        assertNotContains(validTypes, typeHome);
+        assertNotContains(validTypes, typeWork);
+        assertContains(validTypes, typeOther);
+
+        // Add third and fourth home (invalid, but possible)
+        EntityModifier.insertChild(state, kindPhone, typeHome);
+        EntityModifier.insertChild(state, kindPhone, typeHome);
+
+        // Expecting none
+        validTypes = EntityModifier.getValidTypes(state, kindPhone, null);
+        assertNotContains(validTypes, typeHome);
+        assertNotContains(validTypes, typeWork);
+        assertNotContains(validTypes, typeOther);
+    }
+
+    /**
+     * Test {@link EntityModifier#canInsert(EntityDelta, DataKind)} by
+     * inserting various rows.
+     */
+    public void testCanInsert() {
+        // Build a source and pull specific types
+        final ContactsSource source = getSource();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+        final EditType typeWork = EntityModifier.getType(kindPhone, Phone.TYPE_WORK);
+        final EditType typeOther = EntityModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+        // Add first home, first work
+        final EntityDelta state = getEntity();
+        EntityModifier.insertChild(state, kindPhone, typeHome);
+        EntityModifier.insertChild(state, kindPhone, typeWork);
+        assertTrue("Unable to insert", EntityModifier.canInsert(state, kindPhone));
+
+        // Add two other, which puts us just under "5" overall limit
+        EntityModifier.insertChild(state, kindPhone, typeOther);
+        EntityModifier.insertChild(state, kindPhone, typeOther);
+        assertTrue("Unable to insert", EntityModifier.canInsert(state, kindPhone));
+
+        // Add second home, which should push to snug limit
+        EntityModifier.insertChild(state, kindPhone, typeHome);
+        assertFalse("Able to insert", EntityModifier.canInsert(state, kindPhone));
+    }
+
+    /**
+     * Test {@link EntityModifier#getBestValidType(EntityDelta, DataKind)}
+     * by asserting expected best options in various states.
+     */
+    public void testBestValidType() {
+        // Build a source and pull specific types
+        final ContactsSource source = getSource();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+        final EditType typeWork = EntityModifier.getType(kindPhone, Phone.TYPE_WORK);
+        final EditType typeFaxWork = EntityModifier.getType(kindPhone, Phone.TYPE_FAX_WORK);
+        final EditType typeOther = EntityModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+        EditType suggested;
+
+        // Default suggestion should be home
+        final EntityDelta state = getEntity();
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        assertEquals("Unexpected suggestion", typeHome, suggested);
+
+        // Add first home, should now suggest work
+        EntityModifier.insertChild(state, kindPhone, typeHome);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        assertEquals("Unexpected suggestion", typeWork, suggested);
+
+        // Add work fax, should still suggest work
+        EntityModifier.insertChild(state, kindPhone, typeFaxWork);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        assertEquals("Unexpected suggestion", typeWork, suggested);
+
+        // Add other, should still suggest work
+        EntityModifier.insertChild(state, kindPhone, typeOther);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        assertEquals("Unexpected suggestion", typeWork, suggested);
+
+        // Add work, now should suggest other
+        EntityModifier.insertChild(state, kindPhone, typeWork);
+        suggested = EntityModifier.getBestValidType(state, kindPhone, false);
+        assertEquals("Unexpected suggestion", typeOther, suggested);
+    }
+}