Bulk of constraint enforcement code, "best" suggestions.

Wrote getValidTypes() which builds a list of EditTypes that
can be inserted given a AugmentedEntity state, which is our
mechanism for enforcing ContactsSource constraints.  This
also drives canInsert(), which provides the enabled state
for our "add" buttons.

This change also reintroduces "suggested" types, used to
pick the default EditType for newly added entries.  It picks
the first primary type that doesn't already appear, or
otherwise picks the last primary type.  (This gives us a
nice "add" transition down the list of possible types.)

Finally, this change connects all of this logic into the
UI, picking available labels and enabling "add" according
to any constraints.  Also wrote unit tests to verify the
above code is producing the expected results.  These tests
exercise the backbone of our constraint-enforcement logic.
diff --git a/src/com/android/contacts/model/AugmentedEntity.java b/src/com/android/contacts/model/AugmentedEntity.java
index 2cded4e..24264db 100644
--- a/src/com/android/contacts/model/AugmentedEntity.java
+++ b/src/com/android/contacts/model/AugmentedEntity.java
@@ -76,7 +76,7 @@
     public static AugmentedEntity fromBefore(Entity before) {
         final AugmentedEntity entity = new AugmentedEntity();
         entity.mValues = AugmentedValues.fromBefore(before.getEntityValues());
-        entity.mValues.setIdColumn(RawContacts._ID);
+        entity.mValues.setIdColumn("raw_contacts", RawContacts._ID);
         for (NamedContentValues namedValues : before.getSubValues()) {
             entity.addEntry(AugmentedValues.fromBefore(namedValues.values));
         }
@@ -150,6 +150,21 @@
         return null;
     }
 
+    /**
+     * Return the total number of {@link AugmentedValues} contained.
+     */
+    public int getEntryCount(boolean onlyVisible) {
+        int count = 0;
+        for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
+            for (AugmentedValues 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;
 
@@ -330,6 +345,7 @@
     public static class AugmentedValues {
         private ContentValues mBefore;
         private ContentValues mAfter;
+        private String mIdTable = "data";
         private String mIdColumn = BaseColumns._ID;
 
         private AugmentedValues() {
@@ -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() {
@@ -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/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/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index e68d0dc..33ccf6f 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -22,7 +22,10 @@
 
 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;
 
 /**
@@ -30,16 +33,21 @@
  * 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
      * {@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(AugmentedEntity 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(AugmentedEntity state, DataKind kind) {
+        return (getValidTypes(state, kind).size() > 0);
     }
 
     /**
@@ -47,10 +55,95 @@
      * list possible {@link EditType} options available based on
      * {@link ContactsSource}.
      */
-    public static List<EditType> getValidTypes(AugmentedEntity entity, DataKind kind,
+    public static ArrayList<EditType> getValidTypes(AugmentedEntity state, DataKind kind) {
+        return getValidTypes(state, kind, null, true, null);
+    }
+
+    /**
+     * For the given {@link AugmentedEntity} 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(AugmentedEntity 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 AugmentedEntity} 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(AugmentedEntity, DataKind)}.
+     */
+    private static ArrayList<EditType> getValidTypes(AugmentedEntity 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 AugmentedEntity}. 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(AugmentedEntity state, DataKind kind) {
+        final SparseIntArray typeCount = new SparseIntArray();
+
+        // Find all entries for this kind, bailing early if none found
+        final List<AugmentedValues> mimeEntries = state.getMimeEntries(kind.mimeType);
+        if (mimeEntries == null) return typeCount;
+
+        int totalCount = 0;
+        for (AugmentedValues 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;
     }
 
     /**
@@ -67,7 +160,15 @@
      * the possible types.
      */
     public static EditType getCurrentType(AugmentedValues entry, DataKind kind) {
-        final long rawValue = entry.getAsLong(kind.typeColumn);
+        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 @@
     }
 
     /**
+     * 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 EditType getBestValidType(AugmentedEntity 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 AugmentedEntity}. Assumes the caller has already checked
-     * {@link #canInsert(AugmentedEntity, DataKind)}.
+     * {@link AugmentedEntity}. Tries using the best {@link EditType} found
+     * using {@link #getBestValidType(AugmentedEntity, DataKind, boolean)}.
      */
     public static void insertChild(AugmentedEntity 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 AugmentedEntity}, marked with the given {@link EditType}.
+     */
+    public static void insertChild(AugmentedEntity state, DataKind kind, EditType type) {
         final ContentValues after = new ContentValues();
 
         // Our parent CONTACT_ID is provided later
@@ -92,10 +246,9 @@
             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));
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 924241f..058c040 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -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;
@@ -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,7 +157,6 @@
          */
         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();
@@ -160,16 +164,27 @@
             // Build individual editors for each entry
             if (!mState.hasMimeEntries(mKind.mimeType)) return;
             for (AugmentedValues 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();
         }
     }
 
@@ -177,13 +192,28 @@
      * Generic definition of something that edits a {@link Data} row through an
      * {@link AugmentedValues} object.
      */
-    public interface Editor {
+    protected interface Editor {
         /**
          * Prepare this editor for the given {@link AugmentedValues}, 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);
+
+        /**
+         * 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);
     }
 
     /**
@@ -191,7 +221,7 @@
      * the entry. Uses {@link AugmentedValues} 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;
@@ -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.
@@ -247,7 +283,7 @@
             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) {
@@ -454,12 +495,15 @@
 
         public void setValues(DataKind kind, AugmentedValues values, AugmentedEntity 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) {
@@ -468,6 +512,9 @@
 
         public void setValues(DataKind kind, AugmentedValues values, AugmentedEntity state) {
         }
+
+        public void setEditorListener(EditorListener listener) {
+        }
     }
 
 }
diff --git a/tests/src/com/android/contacts/AugmentedEntityTests.java b/tests/src/com/android/contacts/AugmentedEntityTests.java
index e9498e1..aeab4f9 100644
--- a/tests/src/com/android/contacts/AugmentedEntityTests.java
+++ b/tests/src/com/android/contacts/AugmentedEntityTests.java
@@ -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);
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
new file mode 100644
index 0000000..4db9d0b
--- /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.AugmentedEntity;
+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 AugmentedEntity getEntity() {
+        final ContentValues contact = new ContentValues();
+        final Entity before = new Entity(contact);
+        return AugmentedEntity.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(AugmentedEntity, 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 AugmentedEntity 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(AugmentedEntity, 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 AugmentedEntity 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(AugmentedEntity, 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 AugmentedEntity 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);
+    }
+}