Added thin object layer around contact data

This refactoring abstracts out the need to directly
refer to Contacts database columns throughout the code.  Instead,
all of this information is retained in getter/setter methods
within the Contact, RawContact, and DataItem classes and
sub-classes.

ContactLoader.Result class has been pulled to the top level as
the Contact class.

The Entity class has been removed and replaced with a RawContact
class, with getters/setters to raw contact information.
Renamed EntityDelta to RawContactDelta for better understandability
as well as adding getters/setters for specific fields in the
ValuesDelta nested class within EntityDelta.  EntityDeltaList
and EntityModifier have been renamed to RawContactDeltaList and
RawContactModifier with the methods using the RawContact class
directly rather than the Entity class.

Data items for a raw contact are represented by a DataItem object
with specialized getters/setters for subclasses of DataItem.
(e.g., EmailDataItem, PhoneDataItem. etc.).  DataItem is a wrapper
around ContentValues.  This abstracts away the ContactsContract
column fields into getters/setters.

The above refactoring is accompanied with changes throughout the
codebase to use the new Contact, RawContact, and DataItem classes.

Change-Id: I31c1dccd724e9652f9d0af78ca81feb6c5acd71d
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 3acc34c..4333aa4 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -47,10 +47,10 @@
 import android.widget.Toast;
 
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDeltaList;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDeltaList;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.CallerInfoCacheUtils;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
@@ -289,7 +289,7 @@
      * @param rawContactId identifies a writable raw-contact whose photo is to be updated.
      * @param updatedPhotoPath denotes a temporary file containing the contact's new photo.
      */
-    public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
+    public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
             String saveModeExtraKey, int saveMode, boolean isProfile,
             Class<? extends Activity> callbackActivity, String callbackAction, long rawContactId,
             String updatedPhotoPath) {
@@ -306,7 +306,7 @@
      * Contact Editor.
      * @param updatedPhotos maps each raw-contact's ID to the file-path of the new photo.
      */
-    public static Intent createSaveContactIntent(Context context, EntityDeltaList state,
+    public static Intent createSaveContactIntent(Context context, RawContactDeltaList state,
             String saveModeExtraKey, int saveMode, boolean isProfile,
             Class<? extends Activity> callbackActivity, String callbackAction,
             Bundle updatedPhotos) {
@@ -332,13 +332,13 @@
     }
 
     private void saveContact(Intent intent) {
-        EntityDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
+        RawContactDeltaList state = intent.getParcelableExtra(EXTRA_CONTACT_STATE);
         boolean isProfile = intent.getBooleanExtra(EXTRA_SAVE_IS_PROFILE, false);
         Bundle updatedPhotos = intent.getParcelableExtra(EXTRA_UPDATED_PHOTOS);
 
         // Trim any empty fields, and RawContacts, before persisting
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
-        EntityModifier.trimEmpty(state, accountTypes);
+        RawContactModifier.trimEmpty(state, accountTypes);
 
         Uri lookupUri = null;
 
@@ -427,16 +427,16 @@
                     throw new IllegalStateException("Version consistency failed for a new contact");
                 }
 
-                final EntityDeltaList newState = EntityDeltaList.fromQuery(
+                final RawContactDeltaList newState = RawContactDeltaList.fromQuery(
                         isProfile
                                 ? RawContactsEntity.PROFILE_CONTENT_URI
                                 : RawContactsEntity.CONTENT_URI,
                         resolver, sb.toString(), null, null);
-                state = EntityDeltaList.mergeAfter(newState, state);
+                state = RawContactDeltaList.mergeAfter(newState, state);
 
                 // Update the new state to use profile URIs if appropriate.
                 if (isProfile) {
-                    for (EntityDelta delta : state) {
+                    for (RawContactDelta delta : state) {
                         delta.setProfileQueryUri();
                     }
                 }
@@ -518,7 +518,7 @@
     /**
      * Find the ID of an existing or newly-inserted raw-contact.  If none exists, return -1.
      */
-    private long getRawContactId(EntityDeltaList state,
+    private long getRawContactId(RawContactDeltaList state,
             final ArrayList<ContentProviderOperation> diff,
             final ContentProviderResult[] results) {
         long existingRawContactId = state.findRawContactId();
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 0b63345..e8aa1ae 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -33,9 +33,9 @@
 import android.widget.TextView;
 
 import com.android.contacts.activities.DialtactsActivity;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.test.NeededForTesting;
 import com.android.contacts.util.Constants;
 
diff --git a/src/com/android/contacts/SplitAggregateView.java b/src/com/android/contacts/SplitAggregateView.java
index 834635c..1b42ca3 100644
--- a/src/com/android/contacts/SplitAggregateView.java
+++ b/src/com/android/contacts/SplitAggregateView.java
@@ -35,8 +35,8 @@
 import android.widget.ListView;
 import android.widget.TextView;
 
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/src/com/android/contacts/TypePrecedence.java b/src/com/android/contacts/TypePrecedence.java
index b5e0777..b2d8a8f 100644
--- a/src/com/android/contacts/TypePrecedence.java
+++ b/src/com/android/contacts/TypePrecedence.java
@@ -23,14 +23,14 @@
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactModifier;
 import com.android.contacts.util.Constants;
 
 /**
  * This class contains utility functions for determining the precedence of
  * different types associated with contact data items.
  *
- * @deprecated use {@link EntityModifier#getTypePrecedence} instead, since this
+ * @deprecated use {@link RawContactModifier#getTypePrecedence} instead, since this
  *             list isn't {@link Account} based.
  */
 @Deprecated
diff --git a/src/com/android/contacts/ViewNotificationService.java b/src/com/android/contacts/ViewNotificationService.java
index 584176d..3bc5ed2 100644
--- a/src/com/android/contacts/ViewNotificationService.java
+++ b/src/com/android/contacts/ViewNotificationService.java
@@ -23,7 +23,9 @@
 import android.os.IBinder;
 import android.util.Log;
 
-import com.android.contacts.ContactLoader.Result;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.ContactLoader;
+
 
 /**
  * Service that sends out a view notification for a contact. At the moment, this is only
@@ -41,9 +43,9 @@
         // We simply need to start a Loader here. When its done, it will send out the
         // View-Notification automatically.
         final ContactLoader contactLoader = new ContactLoader(this, intent.getData(), true);
-        contactLoader.registerListener(0, new OnLoadCompleteListener<ContactLoader.Result>() {
+        contactLoader.registerListener(0, new OnLoadCompleteListener<Contact>() {
             @Override
-            public void onLoadComplete(Loader<Result> loader, Result data) {
+            public void onLoadComplete(Loader<Contact> loader, Contact data) {
                 try {
                     loader.reset();
                 } catch (RuntimeException e) {
diff --git a/src/com/android/contacts/activities/AttachPhotoActivity.java b/src/com/android/contacts/activities/AttachPhotoActivity.java
index 9ca9cfd..2f7651f 100644
--- a/src/com/android/contacts/activities/AttachPhotoActivity.java
+++ b/src/com/android/contacts/activities/AttachPhotoActivity.java
@@ -30,15 +30,15 @@
 import android.provider.ContactsContract.DisplayPhoto;
 import android.util.Log;
 
-import com.android.contacts.ContactLoader;
-import com.android.contacts.ContactLoader.Result;
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.ContactsUtils;
-import com.android.contacts.model.AccountType;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDeltaList;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.ContactLoader;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDeltaList;
+import com.android.contacts.model.account.AccountType;
 import com.android.contacts.util.ContactPhotoUtils;
 
 import java.io.File;
@@ -131,7 +131,7 @@
         } else if (requestCode == REQUEST_CROP_PHOTO) {
             loadContact(mContactUri, new Listener() {
                 @Override
-                public void onContactLoaded(ContactLoader.Result contact) {
+                public void onContactLoaded(Contact contact) {
                     saveContact(contact);
                 }
             });
@@ -144,10 +144,10 @@
     // instance, the loader doesn't persist across Activity restarts.
     private void loadContact(Uri contactUri, final Listener listener) {
         final ContactLoader loader = new ContactLoader(this, contactUri, true);
-        loader.registerListener(0, new OnLoadCompleteListener<ContactLoader.Result>() {
+        loader.registerListener(0, new OnLoadCompleteListener<Contact>() {
             @Override
             public void onLoadComplete(
-                    Loader<ContactLoader.Result> loader, ContactLoader.Result contact) {
+                    Loader<Contact> loader, Contact contact) {
                 try {
                     loader.reset();
                 }
@@ -161,7 +161,7 @@
     }
 
     private interface Listener {
-        public void onContactLoaded(Result contact);
+        public void onContactLoaded(Contact contact);
     }
 
     /**
@@ -170,11 +170,11 @@
      * - photo has been cropped
      * - contact has been loaded
      */
-    private void saveContact(ContactLoader.Result contact) {
+    private void saveContact(Contact contact) {
 
         // Obtain the raw-contact that we will save to.
-        EntityDeltaList deltaList = contact.createEntityDeltaList();
-        EntityDelta raw = deltaList.getFirstWritableRawContact(this);
+        RawContactDeltaList deltaList = contact.createRawContactDeltaList();
+        RawContactDelta raw = deltaList.getFirstWritableRawContact(this);
         if (raw == null) {
             Log.w(TAG, "no writable raw-contact found");
             return;
@@ -195,13 +195,13 @@
         // the ContactSaveService would not create the new contact, and the
         // full-res photo would fail to be saved to the non-existent contact.
         AccountType account = raw.getRawContactAccountType(this);
-        EntityDelta.ValuesDelta values =
-                EntityModifier.ensureKindExists(raw, account, Photo.CONTENT_ITEM_TYPE);
+        RawContactDelta.ValuesDelta values =
+                RawContactModifier.ensureKindExists(raw, account, Photo.CONTENT_ITEM_TYPE);
         if (values == null) {
             Log.w(TAG, "cannot attach photo to this account type");
             return;
         }
-        values.put(Photo.PHOTO, compressed);
+        values.setPhoto(compressed);
 
         // Finally, invoke the ContactSaveService.
         Log.v(TAG, "all prerequisites met, about to save photo to contact");
diff --git a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
index 18e360f..c8adf95 100644
--- a/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
+++ b/src/com/android/contacts/activities/ConfirmAddDetailActivity.java
@@ -24,7 +24,6 @@
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
 import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.OperationApplicationException;
@@ -62,14 +61,15 @@
 import com.android.contacts.R;
 import com.android.contacts.editor.Editor;
 import com.android.contacts.editor.ViewIdGenerator;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityDeltaList;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContact;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDeltaList;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.DialogManager;
 import com.android.contacts.util.EmptyService;
 
@@ -124,11 +124,11 @@
 
     private QueryHandler mQueryHandler;
 
-    /** {@link EntityDeltaList} for the entire selected contact. */
-    private EntityDeltaList mEntityDeltaList;
+    /** {@link RawContactDeltaList} for the entire selected contact. */
+    private RawContactDeltaList mEntityDeltaList;
 
-    /** {@link EntityDeltaList} for the editable account */
-    private EntityDelta mEntityDelta;
+    /** {@link RawContactDeltaList} for the editable account */
+    private RawContactDelta mRawContactDelta;
 
     private String mMimetype = Phone.CONTENT_ITEM_TYPE;
 
@@ -357,7 +357,7 @@
                 new String[] { String.valueOf(mContactId) }, null);
     }
 
-    private static class QueryEntitiesTask extends AsyncTask<Intent, Void, EntityDeltaList> {
+    private static class QueryEntitiesTask extends AsyncTask<Intent, Void, RawContactDeltaList> {
 
         private ConfirmAddDetailActivity activityTarget;
         private String mSelection;
@@ -367,7 +367,7 @@
         }
 
         @Override
-        protected EntityDeltaList doInBackground(Intent... params) {
+        protected RawContactDeltaList doInBackground(Intent... params) {
 
             final Intent intent = params[0];
 
@@ -400,7 +400,7 @@
 
             // Note that this query does not need to concern itself with whether the contact is
             // the user's profile, since the profile does not show up in the picker.
-            return EntityDeltaList.fromQuery(RawContactsEntity.CONTENT_URI,
+            return RawContactDeltaList.fromQuery(RawContactsEntity.CONTENT_URI,
                     activityTarget.getContentResolver(), mSelection,
                     new String[] { selectionArg }, null);
         }
@@ -425,7 +425,7 @@
         }
 
         @Override
-        protected void onPostExecute(EntityDeltaList entityList) {
+        protected void onPostExecute(RawContactDeltaList entityList) {
             if (activityTarget.isFinishing()) {
                 return;
             }
@@ -567,7 +567,7 @@
         }
     }
 
-    private void setEntityDeltaList(EntityDeltaList entityList) {
+    private void setEntityDeltaList(RawContactDeltaList entityList) {
         if (entityList == null) {
             throw new IllegalStateException();
         }
@@ -578,31 +578,33 @@
         mEntityDeltaList = entityList;
 
         // Find the editable raw_contact.
-        mEntityDelta = mEntityDeltaList.getFirstWritableRawContact(this);
+        mRawContactDelta = mEntityDeltaList.getFirstWritableRawContact(this);
 
         // If no editable raw_contacts are found, create one.
-        if (mEntityDelta == null) {
-            mEntityDelta = addEditableRawContact(this, mEntityDeltaList);
+        if (mRawContactDelta == null) {
+            mRawContactDelta = addEditableRawContact(this, mEntityDeltaList);
 
-            if ((mEntityDelta != null) && VERBOSE_LOGGING) {
+            if ((mRawContactDelta != null) && VERBOSE_LOGGING) {
                 Log.v(TAG, "setEntityDeltaList: created editable raw_contact " + entityList);
             }
         }
 
-        if (mEntityDelta == null) {
+        if (mRawContactDelta == null) {
             // Selected contact is read-only, and there's no editable account.
             mIsReadOnly = true;
             mEditableAccountType = null;
         } else {
             mIsReadOnly = false;
 
-            mEditableAccountType = mEntityDelta.getRawContactAccountType(this);
+            mEditableAccountType = mRawContactDelta.getRawContactAccountType(this);
 
             // Handle any incoming values that should be inserted
             final Bundle extras = getIntent().getExtras();
             if (extras != null && extras.size() > 0) {
-                // If there are any intent extras, add them as additional fields in the EntityDelta.
-                EntityModifier.parseExtras(this, mEditableAccountType, mEntityDelta, extras);
+                // If there are any intent extras, add them as additional fields in the
+                // RawContactDelta.
+                RawContactModifier.parseExtras(this, mEditableAccountType, mRawContactDelta,
+                        extras);
             }
         }
 
@@ -610,12 +612,12 @@
     }
 
     /**
-     * Create an {@link EntityDelta} for a raw_contact on the first editable account found, and add
+     * Create an {@link RawContactDelta} for a raw_contact on the first editable account found, and add
      * to the list.  Also copy the structured name from an existing (read-only) raw_contact to the
      * new one, if any of the read-only contacts has a name.
      */
-    private static EntityDelta addEditableRawContact(Context context,
-            EntityDeltaList entityDeltaList) {
+    private static RawContactDelta addEditableRawContact(Context context,
+            RawContactDeltaList entityDeltaList) {
         // First, see if there's an editable account.
         final AccountTypeManager accounts = AccountTypeManager.getInstance(context);
         final List<AccountWithDataSet> editableAccounts = accounts.getAccounts(true);
@@ -627,44 +629,29 @@
         final AccountType accountType = accounts.getAccountType(
                 editableAccount.type, editableAccount.dataSet);
 
-        // Create a new EntityDelta for the new raw_contact.
-        final ContentValues values = new ContentValues();
-        values.put(RawContacts.ACCOUNT_NAME, editableAccount.name);
-        values.put(RawContacts.ACCOUNT_TYPE, editableAccount.type);
-        values.put(RawContacts.DATA_SET, editableAccount.dataSet);
+        // Create a new RawContactDelta for the new raw_contact.
+        final RawContact rawContact = new RawContact(context);
+        rawContact.setAccount(editableAccount);
 
-        final EntityDelta entityDelta = new EntityDelta(ValuesDelta.fromAfter(values));
+        final RawContactDelta entityDelta = new RawContactDelta(ValuesDelta.fromAfter(
+                rawContact.getValues()));
 
         // Then, copy the structure name from an existing (read-only) raw_contact.
-        for (EntityDelta entity : entityDeltaList) {
+        for (RawContactDelta entity : entityDeltaList) {
             final ArrayList<ValuesDelta> readOnlyNames =
                     entity.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
             if ((readOnlyNames != null) && (readOnlyNames.size() > 0)) {
                 final ValuesDelta readOnlyName = readOnlyNames.get(0);
-
-                final ValuesDelta newName = EntityModifier.ensureKindExists(entityDelta,
+                final ValuesDelta newName = RawContactModifier.ensureKindExists(entityDelta,
                         accountType, StructuredName.CONTENT_ITEM_TYPE);
 
                 // Copy all the data fields.
-                newName.copyStringFrom(readOnlyName, StructuredName.DISPLAY_NAME);
-
-                newName.copyStringFrom(readOnlyName, StructuredName.GIVEN_NAME);
-                newName.copyStringFrom(readOnlyName, StructuredName.FAMILY_NAME);
-                newName.copyStringFrom(readOnlyName, StructuredName.PREFIX);
-                newName.copyStringFrom(readOnlyName, StructuredName.MIDDLE_NAME);
-                newName.copyStringFrom(readOnlyName, StructuredName.SUFFIX);
-
-                newName.copyStringFrom(readOnlyName, StructuredName.PHONETIC_GIVEN_NAME);
-                newName.copyStringFrom(readOnlyName, StructuredName.PHONETIC_MIDDLE_NAME);
-                newName.copyStringFrom(readOnlyName, StructuredName.PHONETIC_FAMILY_NAME);
-
-                newName.copyStringFrom(readOnlyName, StructuredName.FULL_NAME_STYLE);
-                newName.copyStringFrom(readOnlyName, StructuredName.PHONETIC_NAME_STYLE);
+                newName.copyStructuredNameFieldsFrom(readOnlyName);
                 break;
             }
         }
 
-        // Add the new EntityDelta to the list.
+        // Add the new RawContactDelta to the list.
         entityDeltaList.add(entityDelta);
 
         return entityDelta;
@@ -695,11 +682,11 @@
             // Skip kind that are not editable
             if (!kind.editable) continue;
             if (mMimetype.equals(kind.mimeType)) {
-                for (ValuesDelta valuesDelta : mEntityDelta.getMimeEntries(mMimetype)) {
+                for (ValuesDelta valuesDelta : mRawContactDelta.getMimeEntries(mMimetype)) {
                     // Skip entries that aren't visible
                     if (!valuesDelta.isVisible()) continue;
                     if (valuesDelta.isInsert()) {
-                        inflateEditorView(kind, valuesDelta, mEntityDelta);
+                        inflateEditorView(kind, valuesDelta, mRawContactDelta);
                         return;
                     }
                 }
@@ -712,7 +699,7 @@
      * the views corresponding to the the object-model. The resulting EditorView is also added
      * to the end of mEditors
      */
-    private void inflateEditorView(DataKind dataKind, ValuesDelta valuesDelta, EntityDelta state) {
+    private void inflateEditorView(DataKind dataKind, ValuesDelta valuesDelta, RawContactDelta state) {
         final View view = mInflater.inflate(dataKind.editorLayoutResourceId, mEditorContainerView,
                 false);
 
@@ -765,11 +752,11 @@
 
     /**
      * Background task for persisting edited contact data, using the changes
-     * defined by a set of {@link EntityDelta}. This task starts
+     * defined by a set of {@link RawContactDelta}. This task starts
      * {@link EmptyService} to make sure the background thread can finish
      * persisting in cases where the system wants to reclaim our process.
      */
-    private static class PersistTask extends AsyncTask<EntityDeltaList, Void, Integer> {
+    private static class PersistTask extends AsyncTask<RawContactDeltaList, Void, Integer> {
         // In the future, use ContactSaver instead of WeakAsyncTask because of
         // the danger of the activity being null during a save action
         private static final int PERSIST_TRIES = 3;
@@ -799,18 +786,18 @@
         }
 
         @Override
-        protected Integer doInBackground(EntityDeltaList... params) {
+        protected Integer doInBackground(RawContactDeltaList... params) {
             final Context context = activityTarget;
             final ContentResolver resolver = context.getContentResolver();
 
-            EntityDeltaList state = params[0];
+            RawContactDeltaList state = params[0];
 
             if (state == null) {
                 return RESULT_FAILURE;
             }
 
             // Trim any empty fields, and RawContacts, before persisting
-            EntityModifier.trimEmpty(state, mAccountTypeManager);
+            RawContactModifier.trimEmpty(state, mAccountTypeManager);
 
             // Attempt to persist changes
             int tries = 0;
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index ace89df..811b904 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -36,7 +36,6 @@
 import android.view.accessibility.AccessibilityManager;
 import android.widget.Toast;
 
-import com.android.contacts.ContactLoader;
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
@@ -46,7 +45,8 @@
 import com.android.contacts.detail.ContactLoaderFragment;
 import com.android.contacts.detail.ContactLoaderFragment.ContactLoaderFragmentListener;
 import com.android.contacts.interactions.ContactDeletionInteraction;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.PhoneCapabilityTester;
 
 import java.util.ArrayList;
@@ -57,7 +57,7 @@
     /** Shows a toogle button for hiding/showing updates. Don't submit with true */
     private static final boolean DEBUG_TRANSITIONS = false;
 
-    private ContactLoader.Result mContactData;
+    private Contact mContactData;
     private Uri mLookupUri;
 
     private ContactDetailLayoutController mContactDetailLayoutController;
@@ -208,7 +208,7 @@
         }
 
         @Override
-        public void onDetailsLoaded(final ContactLoader.Result result) {
+        public void onDetailsLoaded(final Contact result) {
             if (result == null) {
                 return;
             }
diff --git a/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
index 3e2a893..f5852e5 100644
--- a/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
@@ -31,7 +31,7 @@
 import com.android.contacts.R;
 import com.android.contacts.editor.ContactEditorUtils;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountsListAdapter;
 import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
index 9639d97..77ed857 100644
--- a/src/com/android/contacts/activities/ContactEditorActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -36,9 +36,9 @@
 import com.android.contacts.R;
 import com.android.contacts.editor.ContactEditorFragment;
 import com.android.contacts.editor.ContactEditorFragment.SaveMode;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.DialogManager;
 
 import java.util.ArrayList;
diff --git a/src/com/android/contacts/activities/GroupDetailActivity.java b/src/com/android/contacts/activities/GroupDetailActivity.java
index 9b3743f..492a2ff 100644
--- a/src/com/android/contacts/activities/GroupDetailActivity.java
+++ b/src/com/android/contacts/activities/GroupDetailActivity.java
@@ -33,8 +33,8 @@
 import com.android.contacts.R;
 import com.android.contacts.group.GroupDetailDisplayUtils;
 import com.android.contacts.group.GroupDetailFragment;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 
 public class GroupDetailActivity extends ContactsActivity {
 
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 4ee5ea3..a99ac45 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -47,7 +47,6 @@
 import android.view.ViewGroup;
 import android.widget.Toast;
 
-import com.android.contacts.ContactLoader;
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.ContactsUtils;
@@ -81,7 +80,8 @@
 import com.android.contacts.list.OnContactsUnavailableActionListener;
 import com.android.contacts.list.ProviderStatusWatcher;
 import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.preference.ContactsPreferenceActivity;
 import com.android.contacts.preference.DisplayOptionsPreferenceFragment;
 import com.android.contacts.util.AccountFilterUtil;
@@ -1162,7 +1162,7 @@
         }
 
         @Override
-        public void onDetailsLoaded(final ContactLoader.Result result) {
+        public void onDetailsLoaded(final Contact result) {
             if (result == null) {
                 // Nothing is loaded. Show empty state.
                 mContactDetailLayoutController.showEmptyState();
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
index a5ae7bd..6f3da00 100644
--- a/src/com/android/contacts/activities/PhotoSelectionActivity.java
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -38,7 +38,7 @@
 import com.android.contacts.R;
 import com.android.contacts.detail.PhotoSelectionHandler;
 import com.android.contacts.editor.PhotoActionPopup;
-import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.RawContactDeltaList;
 import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.util.SchedulingUtils;
 
@@ -93,7 +93,7 @@
     private Uri mPhotoUri;
 
     /** Entity delta list of the contact. */
-    private EntityDeltaList mState;
+    private RawContactDeltaList mState;
 
     /** Whether the contact is the user's profile. */
     private boolean mIsProfile;
@@ -167,7 +167,7 @@
         // Pull data out of the intent.
         final Intent intent = getIntent();
         mPhotoUri = intent.getParcelableExtra(PHOTO_URI);
-        mState = (EntityDeltaList) intent.getParcelableExtra(ENTITY_DELTA_LIST);
+        mState = (RawContactDeltaList) intent.getParcelableExtra(ENTITY_DELTA_LIST);
         mIsProfile = intent.getBooleanExtra(IS_PROFILE, false);
         mIsDirectoryContact = intent.getBooleanExtra(IS_DIRECTORY_CONTACT, false);
         mExpandPhoto = intent.getBooleanExtra(EXPAND_PHOTO, false);
@@ -268,7 +268,7 @@
      * @return An intent that can be used to invoke the photo selection activity.
      */
     public static Intent buildIntent(Context context, Uri photoUri, Bitmap photoBitmap,
-            byte[] photoBytes, Rect photoBounds, EntityDeltaList delta, boolean isProfile,
+            byte[] photoBytes, Rect photoBounds, RawContactDeltaList delta, boolean isProfile,
             boolean isDirectoryContact, boolean expandPhotoOnClick) {
         Intent intent = new Intent(context, PhotoSelectionActivity.class);
         if (photoUri != null && photoBitmap != null && photoBytes != null) {
@@ -515,7 +515,7 @@
         private final PhotoActionListener mListener;
 
         private PhotoHandler(
-                Context context, View photoView, int photoMode, EntityDeltaList state) {
+                Context context, View photoView, int photoMode, RawContactDeltaList state) {
             super(context, photoView, photoMode, PhotoSelectionActivity.this.mIsDirectoryContact,
                     state);
             mListener = new PhotoListener();
@@ -536,7 +536,7 @@
         private final class PhotoListener extends PhotoActionListener {
             @Override
             public void onPhotoSelected(Bitmap bitmap) {
-                EntityDeltaList delta = getDeltaForAttachingPhotoToContact();
+                RawContactDeltaList delta = getDeltaForAttachingPhotoToContact();
                 long rawContactId = getWritableEntityId();
                 final String croppedPath = ContactPhotoUtils.pathForCroppedPhoto(
                         PhotoSelectionActivity.this, mCurrentPhotoFile);
diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index 67e8e4e..1908e96 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -17,10 +17,7 @@
 package com.android.contacts.detail;
 
 import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Context;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
@@ -28,8 +25,6 @@
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.DisplayNameSources;
 import android.provider.ContactsContract.StreamItems;
 import android.text.Html;
@@ -44,10 +39,12 @@
 import android.widget.ListView;
 import android.widget.TextView;
 
-import com.android.contacts.ContactLoader;
-import com.android.contacts.ContactLoader.Result;
 import com.android.contacts.ContactPhotoManager;
 import com.android.contacts.R;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.RawContact;
+import com.android.contacts.model.dataitem.DataItem;
+import com.android.contacts.model.dataitem.OrganizationDataItem;
 import com.android.contacts.preference.ContactsPreferences;
 import com.android.contacts.util.ContactBadgeUtil;
 import com.android.contacts.util.HtmlUtils;
@@ -55,13 +52,14 @@
 import com.android.contacts.util.StreamItemEntry;
 import com.android.contacts.util.StreamItemPhotoEntry;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Iterables;
 
 import java.util.List;
 
 /**
  * This class contains utility methods to bind high-level contact details
  * (meaning name, phonetic name, job, and attribution) from a
- * {@link ContactLoader.Result} data object to appropriate {@link View}s.
+ * {@link Contact} data object to appropriate {@link View}s.
  */
 public class ContactDetailDisplayUtils {
     private static final String TAG = "ContactDetailDisplayUtils";
@@ -95,7 +93,7 @@
      * Returns the display name of the contact, using the current display order setting.
      * Returns res/string/missing_name if there is no display name.
      */
-    public static CharSequence getDisplayName(Context context, Result contactData) {
+    public static CharSequence getDisplayName(Context context, Contact contactData) {
         CharSequence displayName = contactData.getDisplayName();
         CharSequence altDisplayName = contactData.getAltDisplayName();
         ContactsPreferences prefs = new ContactsPreferences(context);
@@ -115,7 +113,7 @@
     /**
      * Returns the phonetic name of the contact or null if there isn't one.
      */
-    public static String getPhoneticName(Context context, Result contactData) {
+    public static String getPhoneticName(Context context, Contact contactData) {
         String phoneticName = contactData.getPhoneticName();
         if (!TextUtils.isEmpty(phoneticName)) {
             return phoneticName;
@@ -127,7 +125,7 @@
      * Returns the attribution string for the contact, which may specify the contact directory that
      * the contact came from. Returns null if there is none applicable.
      */
-    public static String getAttribution(Context context, Result contactData) {
+    public static String getAttribution(Context context, Contact contactData) {
         if (contactData.isDirectoryEntry()) {
             String directoryDisplayName = contactData.getDirectoryDisplayName();
             String directoryType = contactData.getDirectoryType();
@@ -143,40 +141,37 @@
      * Returns the organization of the contact. If several organizations are given,
      * the first one is used. Returns null if not applicable.
      */
-    public static String getCompany(Context context, Result contactData) {
+    public static String getCompany(Context context, Contact contactData) {
         final boolean displayNameIsOrganization = contactData.getDisplayNameSource()
                 == DisplayNameSources.ORGANIZATION;
-        for (Entity entity : contactData.getEntities()) {
-            for (NamedContentValues subValue : entity.getSubValues()) {
-                final ContentValues entryValues = subValue.values;
-                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
-
-                if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    final String company = entryValues.getAsString(Organization.COMPANY);
-                    final String title = entryValues.getAsString(Organization.TITLE);
-                    final String combined;
-                    // We need to show company and title in a combined string. However, if the
-                    // DisplayName is already the organization, it mirrors company or (if company
-                    // is empty title). Make sure we don't show what's already shown as DisplayName
-                    if (TextUtils.isEmpty(company)) {
-                        combined = displayNameIsOrganization ? null : title;
+        for (RawContact rawContact : contactData.getRawContacts()) {
+            for (DataItem dataItem : Iterables.filter(
+                    rawContact.getDataItems(), OrganizationDataItem.class)) {
+                OrganizationDataItem organization = (OrganizationDataItem) dataItem;
+                final String company = organization.getCompany();
+                final String title = organization.getTitle();
+                final String combined;
+                // We need to show company and title in a combined string. However, if the
+                // DisplayName is already the organization, it mirrors company or (if company
+                // is empty title). Make sure we don't show what's already shown as DisplayName
+                if (TextUtils.isEmpty(company)) {
+                    combined = displayNameIsOrganization ? null : title;
+                } else {
+                    if (TextUtils.isEmpty(title)) {
+                        combined = displayNameIsOrganization ? null : company;
                     } else {
-                        if (TextUtils.isEmpty(title)) {
-                            combined = displayNameIsOrganization ? null : company;
+                        if (displayNameIsOrganization) {
+                            combined = title;
                         } else {
-                            if (displayNameIsOrganization) {
-                                combined = title;
-                            } else {
-                                combined = context.getString(
-                                        R.string.organization_company_and_title,
-                                        company, title);
-                            }
+                            combined = context.getString(
+                                    R.string.organization_company_and_title,
+                                    company, title);
                         }
                     }
+                }
 
-                    if (!TextUtils.isEmpty(combined)) {
-                        return combined;
-                    }
+                if (!TextUtils.isEmpty(combined)) {
+                    return combined;
                 }
             }
         }
@@ -225,7 +220,7 @@
     /**
      * Set the social snippet text. If there isn't one, then set the view to gone.
      */
-    public static void setSocialSnippet(Context context, Result contactData, TextView statusView,
+    public static void setSocialSnippet(Context context, Contact contactData, TextView statusView,
             ImageView statusPhotoView) {
         if (statusView == null) {
             return;
@@ -378,7 +373,7 @@
      * Sets the display name of this contact to the given {@link TextView}. If
      * there is none, then set the view to gone.
      */
-    public static void setDisplayName(Context context, Result contactData, TextView textView) {
+    public static void setDisplayName(Context context, Contact contactData, TextView textView) {
         if (textView == null) {
             return;
         }
@@ -389,7 +384,7 @@
      * Sets the company and job title of this contact to the given {@link TextView}. If
      * there is none, then set the view to gone.
      */
-    public static void setCompanyName(Context context, Result contactData, TextView textView) {
+    public static void setCompanyName(Context context, Contact contactData, TextView textView) {
         if (textView == null) {
             return;
         }
@@ -400,7 +395,7 @@
      * Sets the phonetic name of this contact to the given {@link TextView}. If
      * there is none, then set the view to gone.
      */
-    public static void setPhoneticName(Context context, Result contactData, TextView textView) {
+    public static void setPhoneticName(Context context, Contact contactData, TextView textView) {
         if (textView == null) {
             return;
         }
@@ -411,7 +406,7 @@
      * Sets the attribution contact to the given {@link TextView}. If
      * there is none, then set the view to gone.
      */
-    public static void setAttribution(Context context, Result contactData, TextView textView) {
+    public static void setAttribution(Context context, Contact contactData, TextView textView) {
         if (textView == null) {
             return;
         }
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index 4fee26e..f66466d 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -22,8 +22,6 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
@@ -36,23 +34,13 @@
 import android.os.ServiceManager;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Event;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Relation;
-import android.provider.ContactsContract.CommonDataKinds.SipAddress;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.DisplayNameSources;
-import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.StatusUpdates;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
@@ -83,7 +71,6 @@
 
 import com.android.contacts.Collapser;
 import com.android.contacts.Collapser.Collapsible;
-import com.android.contacts.ContactLoader;
 import com.android.contacts.ContactPresenceIconUtil;
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsUtils;
@@ -92,15 +79,31 @@
 import com.android.contacts.TypePrecedence;
 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
 import com.android.contacts.editor.SelectAccountDialogFragment;
-import com.android.contacts.model.AccountType;
-import com.android.contacts.model.AccountType.EditType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityDeltaList;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.RawContact;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDeltaList;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountType.EditType;
+import com.android.contacts.model.account.AccountWithDataSet;
+import com.android.contacts.model.dataitem.DataItem;
+import com.android.contacts.model.dataitem.DataKind;
+import com.android.contacts.model.dataitem.EmailDataItem;
+import com.android.contacts.model.dataitem.EventDataItem;
+import com.android.contacts.model.dataitem.GroupMembershipDataItem;
+import com.android.contacts.model.dataitem.ImDataItem;
+import com.android.contacts.model.dataitem.NicknameDataItem;
+import com.android.contacts.model.dataitem.NoteDataItem;
+import com.android.contacts.model.dataitem.OrganizationDataItem;
+import com.android.contacts.model.dataitem.PhoneDataItem;
+import com.android.contacts.model.dataitem.RelationDataItem;
+import com.android.contacts.model.dataitem.SipAddressDataItem;
+import com.android.contacts.model.dataitem.StructuredNameDataItem;
+import com.android.contacts.model.dataitem.StructuredPostalDataItem;
+import com.android.contacts.model.dataitem.WebsiteDataItem;
 import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.util.ClipboardUtils;
 import com.android.contacts.util.Constants;
@@ -110,6 +113,7 @@
 import com.android.contacts.util.StructuredPostalUtils;
 import com.android.internal.telephony.ITelephony;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Iterables;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -137,7 +141,7 @@
     private Uri mLookupUri;
     private Listener mListener;
 
-    private ContactLoader.Result mContactData;
+    private Contact mContactData;
     private ViewGroup mStaticPhotoContainer;
     private View mPhotoTouchOverlay;
     private ListView mListView;
@@ -187,9 +191,8 @@
     private Parcelable mListState;
 
     /**
-     * A list of distinct contact IDs included in the current contact.
+     * Lists of specific types of entries to be shown in contact details.
      */
-    private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
     private ArrayList<DetailViewEntry> mPhoneEntries = new ArrayList<DetailViewEntry>();
     private ArrayList<DetailViewEntry> mSmsEntries = new ArrayList<DetailViewEntry>();
     private ArrayList<DetailViewEntry> mEmailEntries = new ArrayList<DetailViewEntry>();
@@ -335,7 +338,7 @@
         return mListener;
     }
 
-    protected ContactLoader.Result getContactData() {
+    protected Contact getContactData() {
         return mContactData;
     }
 
@@ -362,7 +365,7 @@
         setData(null, null);
     }
 
-    public void setData(Uri lookupUri, ContactLoader.Result result) {
+    public void setData(Uri lookupUri, Contact result) {
         mLookupUri = lookupUri;
         mContactData = result;
         bindData();
@@ -530,65 +533,47 @@
         // Clear out the old entries
         mAllEntries.clear();
 
-        mRawContactIds.clear();
-
         mPrimaryPhoneUri = null;
 
-        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-
         // Build up method entries
         if (mContactData == null) {
             return;
         }
 
         ArrayList<String> groups = new ArrayList<String>();
-        for (Entity entity: mContactData.getEntities()) {
-            final ContentValues entValues = entity.getEntityValues();
-            final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = entValues.getAsString(RawContacts.DATA_SET);
-            final long rawContactId = entValues.getAsLong(RawContacts._ID);
+        for (RawContact rawContact: mContactData.getRawContacts()) {
+            final long rawContactId = rawContact.getId();
+            for (DataItem dataItem : rawContact.getDataItems()) {
+                dataItem.setRawContactId(rawContactId);
 
-            if (!mRawContactIds.contains(rawContactId)) {
-                mRawContactIds.add(rawContactId);
-            }
+                if (dataItem.getMimeType() == null) continue;
 
-            AccountType type = accountTypes.getAccountType(accountType, dataSet);
-
-            for (NamedContentValues subValue : entity.getSubValues()) {
-                final ContentValues entryValues = subValue.values;
-                entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
-
-                final long dataId = entryValues.getAsLong(Data._ID);
-                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
-                if (mimeType == null) continue;
-
-                if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    Long groupId = entryValues.getAsLong(GroupMembership.GROUP_ROW_ID);
+                if (dataItem instanceof GroupMembershipDataItem) {
+                    GroupMembershipDataItem groupMembership =
+                            (GroupMembershipDataItem) dataItem;
+                    Long groupId = groupMembership.getGroupRowId();
                     if (groupId != null) {
                         handleGroupMembership(groups, mContactData.getGroupMetaData(), groupId);
                     }
                     continue;
                 }
 
-                final DataKind kind = accountTypes.getKindOrFallback(
-                        accountType, dataSet, mimeType);
+                final DataKind kind = dataItem.getDataKind();
                 if (kind == null) continue;
 
-                final DetailViewEntry entry = DetailViewEntry.fromValues(mContext, mimeType, kind,
-                        dataId, entryValues, mContactData.isDirectoryEntry(),
-                        mContactData.getDirectoryId());
+                final DetailViewEntry entry = DetailViewEntry.fromValues(mContext, dataItem,
+                        mContactData.isDirectoryEntry(), mContactData.getDirectoryId());
                 entry.maxLines = kind.maxLinesForDisplay;
 
                 final boolean hasData = !TextUtils.isEmpty(entry.data);
-                Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
-                final boolean isSuperPrimary = superPrimary != null && superPrimary != 0;
+                final boolean isSuperPrimary = dataItem.isSuperPrimary();
 
-                if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                if (dataItem instanceof StructuredNameDataItem) {
                     // Always ignore the name. It is shown in the header if set
-                } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof PhoneDataItem && hasData) {
+                    PhoneDataItem phone = (PhoneDataItem) dataItem;
                     // Build phone entries
-                    String phoneNumberE164 =
-                            entryValues.getAsString(Phone.NORMALIZED_NUMBER);
+                    String phoneNumberE164 = phone.getNormalizedNumber();
                     entry.data = PhoneNumberUtils.formatNumber(
                             entry.data, phoneNumberE164, mDefaultCountryIso);
                     final Intent phoneIntent = mHasPhone ?
@@ -623,7 +608,7 @@
                         // add to end of list
                         mPhoneEntries.add(entry);
                     }
-                } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof EmailDataItem && hasData) {
                     // Build email entries
                     entry.intent = new Intent(Intent.ACTION_SENDTO,
                             Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
@@ -638,24 +623,23 @@
                     // When Email rows have status, create additional Im row
                     final DataStatus status = mContactData.getStatuses().get(entry.id);
                     if (status != null) {
-                        final String imMime = Im.CONTENT_ITEM_TYPE;
-                        final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
-                                imMime);
-                        final DetailViewEntry imEntry = DetailViewEntry.fromValues(mContext, imMime,
-                                imKind, dataId, entryValues, mContactData.isDirectoryEntry(),
-                                mContactData.getDirectoryId());
-                        buildImActions(mContext, imEntry, entryValues);
+                        EmailDataItem email = (EmailDataItem) dataItem;
+                        ImDataItem im = ImDataItem.createFromEmail(email);
+
+                        final DetailViewEntry imEntry = DetailViewEntry.fromValues(mContext, im,
+                                mContactData.isDirectoryEntry(), mContactData.getDirectoryId());
+                        buildImActions(mContext, imEntry, im);
                         imEntry.setPresence(status.getPresence());
-                        imEntry.maxLines = imKind.maxLinesForDisplay;
+                        imEntry.maxLines = kind.maxLinesForDisplay;
                         mImEntries.add(imEntry);
                     }
-                } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof StructuredPostalDataItem && hasData) {
                     // Build postal entries
                     entry.intent = StructuredPostalUtils.getViewPostalAddressIntent(entry.data);
                     mPostalEntries.add(entry);
-                } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof ImDataItem && hasData) {
                     // Build IM entries
-                    buildImActions(mContext, entry, entryValues);
+                    buildImActions(mContext, entry, (ImDataItem) dataItem);
 
                     // Apply presence when available
                     final DataStatus status = mContactData.getStatuses().get(entry.id);
@@ -663,10 +647,10 @@
                         entry.setPresence(status.getPresence());
                     }
                     mImEntries.add(entry);
-                } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                } else if (dataItem instanceof OrganizationDataItem) {
                     // Organizations are not shown. The first one is shown in the header
                     // and subsequent ones are not supported anymore
-                } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof NicknameDataItem && hasData) {
                     // Build nickname entries
                     final boolean isNameRawContact =
                         (mContactData.getNameRawContactId() == rawContactId);
@@ -679,11 +663,11 @@
                         entry.uri = null;
                         mNicknameEntries.add(entry);
                     }
-                } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof NoteDataItem && hasData) {
                     // Build note entries
                     entry.uri = null;
                     mNoteEntries.add(entry);
-                } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof WebsiteDataItem && hasData) {
                     // Build Website entries
                     entry.uri = null;
                     try {
@@ -694,7 +678,7 @@
                         Log.e(TAG, "Couldn't parse website: " + entry.data);
                     }
                     mWebsiteEntries.add(entry);
-                } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof SipAddressDataItem && hasData) {
                     // Build SipAddress entries
                     entry.uri = null;
                     if (mHasSip) {
@@ -710,11 +694,11 @@
                     // (Then, we'd also update FallbackAccountType.java to set
                     // secondary=false for this field, and tweak the weight
                     // of its DataKind.)
-                } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof EventDataItem && hasData) {
                     entry.data = DateUtils.formatDate(mContext, entry.data);
                     entry.uri = null;
                     mEventEntries.add(entry);
-                } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                } else if (dataItem instanceof RelationDataItem && hasData) {
                     entry.intent = new Intent(Intent.ACTION_SEARCH);
                     entry.intent.putExtra(SearchManager.QUERY, entry.data);
                     entry.intent.setType(Contacts.CONTENT_TYPE);
@@ -724,14 +708,12 @@
                     entry.intent = new Intent(Intent.ACTION_VIEW);
                     entry.intent.setDataAndType(entry.uri, entry.mimetype);
 
-                    if (kind.actionBody != null) {
-                         CharSequence body = kind.actionBody.inflateUsing(mContext, entryValues);
-                         entry.data = (body == null) ? null : body.toString();
-                    }
+                    entry.data = dataItem.buildDataString();
 
                     if (!TextUtils.isEmpty(entry.data)) {
                         // If the account type exists in the hash map, add it as another entry for
                         // that account type
+                        AccountType type = dataItem.getAccountType();
                         if (mOtherEntriesMap.containsKey(type)) {
                             List<DetailViewEntry> listEntries = mOtherEntriesMap.get(type);
                             listEntries.add(entry);
@@ -960,37 +942,27 @@
         }
     }
 
-    private static String buildDataString(DataKind kind, ContentValues values,
-            Context context) {
-        if (kind.actionBody == null) {
-            return null;
-        }
-        CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
-        return actionBody == null ? null : actionBody.toString();
-    }
-
     /**
      * Writes the Instant Messaging action into the given entry value.
      */
     @VisibleForTesting
     public static void buildImActions(Context context, DetailViewEntry entry,
-            ContentValues values) {
-        final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(values.getAsString(Data.MIMETYPE));
+            ImDataItem im) {
+        final boolean isEmail = im.isCreatedFromEmail();
 
-        if (!isEmail && !isProtocolValid(values)) {
+        if (!isEmail && !im.isProtocolValid()) {
             return;
         }
 
-        final String data = values.getAsString(isEmail ? Email.DATA : Im.DATA);
+        final String data = im.getData();
         if (TextUtils.isEmpty(data)) {
             return;
         }
 
-        final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : values.getAsInteger(Im.PROTOCOL);
+        final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
 
         if (protocol == Im.PROTOCOL_GOOGLE_TALK) {
-            final Integer chatCapabilityObj = values.getAsInteger(Im.CHAT_CAPABILITY);
-            final int chatCapability = chatCapabilityObj == null ? 0 : chatCapabilityObj;
+            final int chatCapability = im.getChatCapability();
             entry.chatCapability = chatCapability;
             entry.typeString = Im.getProtocolLabel(context.getResources(), Im.PROTOCOL_GOOGLE_TALK,
                     null).toString();
@@ -1011,7 +983,7 @@
             }
         } else {
             // Build an IM Intent
-            String host = values.getAsString(Im.CUSTOM_PROTOCOL);
+            String host = im.getCustomProtocol();
 
             if (protocol != Im.PROTOCOL_CUSTOM) {
                 // Try bringing in a well-known host for specific protocols
@@ -1027,19 +999,6 @@
         }
     }
 
-    private static boolean isProtocolValid(ContentValues values) {
-        String protocolString = values.getAsString(Im.PROTOCOL);
-        if (protocolString == null) {
-            return false;
-        }
-        try {
-            Integer.valueOf(protocolString);
-        } catch (NumberFormatException e) {
-            return false;
-        }
-        return true;
-    }
-
     /**
      * Show a list popup.  Used for "popup-able" entry, such as "More networks".
      */
@@ -1246,6 +1205,38 @@
 
         private boolean mIsInSubSection = false;
 
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("== DetailViewEntry ==\n");
+            sb.append("  type: " + type + "\n");
+            sb.append("  kind: " + kind + "\n");
+            sb.append("  typeString: " + typeString + "\n");
+            sb.append("  data: " + data + "\n");
+            sb.append("  uri: " + uri.toString() + "\n");
+            sb.append("  maxLines: " + maxLines + "\n");
+            sb.append("  mimetype: " + mimetype + "\n");
+            sb.append("  isPrimary: " + (isPrimary ? "true" : "false") + "\n");
+            sb.append("  secondaryActionIcon: " + secondaryActionIcon + "\n");
+            sb.append("  secondaryActionDescription: " + secondaryActionDescription + "\n");
+            if (intent == null) {
+                sb.append("  intent: " + intent.toString() + "\n");
+            } else {
+                sb.append("  intent: " + intent.toString() + "\n");
+            }
+            if (secondaryIntent == null) {
+                sb.append("  secondaryIntent: (null)\n");
+            } else {
+                sb.append("  secondaryIntent: " + secondaryIntent.toString() + "\n");
+            }
+            sb.append("  ids: " + Iterables.toString(ids) + "\n");
+            sb.append("  collapseCount: " + collapseCount + "\n");
+            sb.append("  presence: " + presence + "\n");
+            sb.append("  chatCapability: " + chatCapability + "\n");
+            sb.append("  mIsInSubsection: " + (mIsInSubSection ? "true" : "false") + "\n");
+            return sb.toString();
+        }
+
         DetailViewEntry() {
             super(ViewAdapter.VIEW_TYPE_DETAIL_ENTRY);
             isEnabled = true;
@@ -1254,34 +1245,34 @@
         /**
          * Build new {@link DetailViewEntry} and populate from the given values.
          */
-        public static DetailViewEntry fromValues(Context context, String mimeType, DataKind kind,
-                long dataId, ContentValues values, boolean isDirectoryEntry, long directoryId) {
+        public static DetailViewEntry fromValues(Context context, DataItem item,
+                boolean isDirectoryEntry, long directoryId) {
             final DetailViewEntry entry = new DetailViewEntry();
-            entry.id = dataId;
+            entry.id = item.getId();
             entry.context = context;
             entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
             if (isDirectoryEntry) {
                 entry.uri = entry.uri.buildUpon().appendQueryParameter(
                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
             }
-            entry.mimetype = mimeType;
-            entry.kind = (kind.titleRes == -1 || kind.titleRes == 0) ? ""
-                    : context.getString(kind.titleRes);
-            entry.data = buildDataString(kind, values, context);
+            entry.mimetype = item.getMimeType();
+            entry.kind = item.getKindString();
+            entry.data = item.buildDataString();
 
-            if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
-                entry.type = values.getAsInteger(kind.typeColumn);
+            if (item.hasKindTypeColumn()) {
+                entry.type = item.getKindTypeColumn();
 
                 // get type string
                 entry.typeString = "";
-                for (EditType type : kind.typeList) {
+                for (EditType type : item.getDataKind().typeList) {
                     if (type.rawValue == entry.type) {
                         if (type.customColumn == null) {
                             // Non-custom type. Get its description from the resource
                             entry.typeString = context.getString(type.labelRes);
                         } else {
                             // Custom type. Read it from the database
-                            entry.typeString = values.getAsString(type.customColumn);
+                            entry.typeString =
+                                    item.getContentValues().getAsString(type.customColumn);
                         }
                         break;
                     }
@@ -1996,7 +1987,7 @@
             if (mContactData.isUserProfile()) return false;
 
             // Only if exactly one raw contact
-            if (mContactData.getEntities().size() != 1) return false;
+            if (mContactData.getRawContacts().size() != 1) return false;
 
             // test if the default group is assigned
             final List<GroupMetaData> groups = mContactData.getGroupMetaData();
@@ -2008,28 +1999,20 @@
             final long defaultGroupId = getDefaultGroupId(groups);
             if (defaultGroupId == -1) return false;
 
-            final Entity rawContactEntity = mContactData.getEntities().get(0);
-            ContentValues rawValues = rawContactEntity.getEntityValues();
-            final String accountType = rawValues.getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = rawValues.getAsString(RawContacts.DATA_SET);
-            final AccountTypeManager accountTypes =
-                    AccountTypeManager.getInstance(mContext);
-            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+            final RawContact rawContact = (RawContact) mContactData.getRawContacts().get(0);
+            final AccountType type = rawContact.getAccountType();
             // Offline or non-writeable account? Nothing to fix
             if (type == null || !type.areContactsWritable()) return false;
 
             // Check whether the contact is in the default group
             boolean isInDefaultGroup = false;
-            for (NamedContentValues subValue : rawContactEntity.getSubValues()) {
-                final String mimeType = subValue.values.getAsString(Data.MIMETYPE);
-
-                if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    final Long groupId =
-                            subValue.values.getAsLong(GroupMembership.GROUP_ROW_ID);
-                    if (groupId == defaultGroupId) {
-                        isInDefaultGroup = true;
-                        break;
-                    }
+            for (DataItem dataItem : Iterables.filter(
+                    rawContact.getDataItems(), GroupMembershipDataItem.class)) {
+                GroupMembershipDataItem groupMembership = (GroupMembershipDataItem) dataItem;
+                final Long groupId = groupMembership.getGroupRowId();
+                if (groupId == defaultGroupId) {
+                    isInDefaultGroup = true;
+                    break;
                 }
             }
 
@@ -2049,19 +2032,16 @@
             if (defaultGroupId == -1) return;
 
             // add the group membership to the current state
-            final EntityDeltaList contactDeltaList = mContactData.createEntityDeltaList();
-            final EntityDelta rawContactEntityDelta = contactDeltaList.get(0);
+            final RawContactDeltaList contactDeltaList = mContactData.createRawContactDeltaList();
+            final RawContactDelta rawContactEntityDelta = contactDeltaList.get(0);
 
             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-            final ValuesDelta values = rawContactEntityDelta.getValues();
-            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = values.getAsString(RawContacts.DATA_SET);
-            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+            final AccountType type = rawContactEntityDelta.getAccountType(accountTypes);
             final DataKind groupMembershipKind = type.getKindForMimetype(
                     GroupMembership.CONTENT_ITEM_TYPE);
-            final ValuesDelta entry = EntityModifier.insertChild(rawContactEntityDelta,
+            final ValuesDelta entry = RawContactModifier.insertChild(rawContactEntityDelta,
                     groupMembershipKind);
-            entry.put(GroupMembership.GROUP_ROW_ID, defaultGroupId);
+            entry.setGroupRowId(defaultGroupId);
 
             // and fire off the intent. we don't need a callback, as the database listener
             // should update the ui
@@ -2198,7 +2178,7 @@
         private final LayoutInflater mInflater;
         private final ArrayList<AccountType> mAccountTypes;
 
-        public InvitableAccountTypesAdapter(Context context, ContactLoader.Result contactData) {
+        public InvitableAccountTypesAdapter(Context context, Contact contactData) {
             mContext = context;
             mInflater = LayoutInflater.from(context);
             final List<AccountType> types = contactData.getInvitableAccountTypes();
diff --git a/src/com/android/contacts/detail/ContactDetailLayoutController.java b/src/com/android/contacts/detail/ContactDetailLayoutController.java
index 8a87231..fca426c 100644
--- a/src/com/android/contacts/detail/ContactDetailLayoutController.java
+++ b/src/com/android/contacts/detail/ContactDetailLayoutController.java
@@ -34,10 +34,10 @@
 import android.widget.AbsListView;
 import android.widget.AbsListView.OnScrollListener;
 
-import com.android.contacts.ContactLoader;
 import com.android.contacts.NfcHandler;
 import com.android.contacts.R;
 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
+import com.android.contacts.model.Contact;
 import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contacts.util.UriUtils;
 import com.android.contacts.widget.FrameLayoutWithOverlay;
@@ -102,7 +102,7 @@
 
     private final ContactDetailFragment.Listener mContactDetailFragmentListener;
 
-    private ContactLoader.Result mContactData;
+    private Contact mContactData;
     private Uri mContactUri;
 
     private boolean mTabCarouselIsAnimating;
@@ -270,7 +270,7 @@
         }
     }
 
-    public void setContactData(ContactLoader.Result data) {
+    public void setContactData(Contact data) {
         final boolean contactWasLoaded;
         final boolean contactHadUpdates;
         final boolean isDifferentContact;
diff --git a/src/com/android/contacts/detail/ContactDetailPhotoSetter.java b/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
index 126c43a..eb832e9 100644
--- a/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
+++ b/src/com/android/contacts/detail/ContactDetailPhotoSetter.java
@@ -25,10 +25,10 @@
 import android.view.View.OnClickListener;
 import android.widget.ImageView;
 
-import com.android.contacts.ContactLoader.Result;
 import com.android.contacts.ContactPhotoManager;
 import com.android.contacts.activities.PhotoSelectionActivity;
-import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.RawContactDeltaList;
 import com.android.contacts.util.ImageViewDrawableSetter;
 
 /**
@@ -36,7 +36,7 @@
  * photo.
  */
 public class ContactDetailPhotoSetter extends ImageViewDrawableSetter {
-    public OnClickListener setupContactPhotoForClick(Context context, Result contactData,
+    public OnClickListener setupContactPhotoForClick(Context context, Contact contactData,
             ImageView photoView, boolean expandPhotoOnClick) {
         setTarget(photoView);
         Bitmap bitmap = setCompressedImage(contactData.getPhotoBinaryData());
@@ -46,12 +46,12 @@
     private static final class PhotoClickListener implements OnClickListener {
 
         private final Context mContext;
-        private final Result mContactData;
+        private final Contact mContactData;
         private final Bitmap mPhotoBitmap;
         private final byte[] mPhotoBytes;
         private final boolean mExpandPhotoOnClick;
 
-        public PhotoClickListener(Context context, Result contactData, Bitmap photoBitmap,
+        public PhotoClickListener(Context context, Contact contactData, Bitmap photoBitmap,
                 byte[] photoBytes, boolean expandPhotoOnClick) {
             mContext = context;
             mContactData = contactData;
@@ -63,7 +63,7 @@
         @Override
         public void onClick(View v) {
             // Assemble the intent.
-            EntityDeltaList delta = mContactData.createEntityDeltaList();
+            RawContactDeltaList delta = mContactData.createRawContactDeltaList();
 
             // Find location and bounds of target view, adjusting based on the
             // assumed local density.
@@ -96,7 +96,7 @@
         }
     }
 
-    private OnClickListener setupClickListener(Context context, Result contactData, Bitmap bitmap,
+    private OnClickListener setupClickListener(Context context, Contact contactData, Bitmap bitmap,
             boolean expandPhotoOnClick) {
         final ImageView target = getTarget();
         if (target == null) return null;
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 3191402..540f001 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -29,8 +29,8 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
-import com.android.contacts.ContactLoader;
 import com.android.contacts.R;
+import com.android.contacts.model.Contact;
 import com.android.contacts.util.MoreMath;
 import com.android.contacts.util.SchedulingUtils;
 
@@ -464,7 +464,7 @@
      * Loads the data from the Loader-Result. This is the only function that has to be called
      * from the outside to fully setup the View
      */
-    public void loadData(ContactLoader.Result contactData) {
+    public void loadData(Contact contactData) {
         if (contactData == null) return;
 
         // TODO: Move this into the {@link CarouselTab} class when the updates
diff --git a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
index 9ae614a..88c30c2 100644
--- a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
@@ -28,19 +28,19 @@
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.ListView;
 
-import com.android.contacts.ContactLoader;
 import com.android.contacts.R;
 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
 import com.android.contacts.detail.ContactDetailDisplayUtils.StreamPhotoTag;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.account.AccountType;
 import com.android.contacts.util.StreamItemEntry;
 
 public class ContactDetailUpdatesFragment extends ListFragment implements FragmentKeyListener {
 
     private static final String TAG = "ContactDetailUpdatesFragment";
 
-    private ContactLoader.Result mContactData;
+    private Contact mContactData;
     private Uri mLookupUri;
 
     private LayoutInflater mInflater;
@@ -121,7 +121,7 @@
         }
     }
 
-    public void setData(Uri lookupUri, ContactLoader.Result result) {
+    public void setData(Uri lookupUri, Contact result) {
         if (result == null) {
             return;
         }
diff --git a/src/com/android/contacts/detail/ContactLoaderFragment.java b/src/com/android/contacts/detail/ContactLoaderFragment.java
index 787390c..6a69744 100644
--- a/src/com/android/contacts/detail/ContactLoaderFragment.java
+++ b/src/com/android/contacts/detail/ContactLoaderFragment.java
@@ -39,12 +39,13 @@
 import android.view.ViewGroup;
 import android.widget.Toast;
 
-import com.android.contacts.ContactLoader;
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.R;
 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
 import com.android.contacts.list.ShortcutIntentBuilder;
 import com.android.contacts.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.ContactLoader;
 import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.internal.util.Objects;
 
@@ -84,7 +85,7 @@
         /**
          * Contact details have finished loading.
          */
-        public void onDetailsLoaded(ContactLoader.Result result);
+        public void onDetailsLoaded(Contact result);
 
         /**
          * User decided to go to Edit-Mode
@@ -107,7 +108,7 @@
     private Uri mLookupUri;
     private ContactLoaderFragmentListener mListener;
 
-    private ContactLoader.Result mContactData;
+    private Contact mContactData;
 
     public ContactLoaderFragment() {
     }
@@ -179,10 +180,10 @@
     /**
      * The listener for the detail loader
      */
-    private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDetailLoaderListener =
-            new LoaderCallbacks<ContactLoader.Result>() {
+    private final LoaderManager.LoaderCallbacks<Contact> mDetailLoaderListener =
+            new LoaderCallbacks<Contact>() {
         @Override
-        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+        public Loader<Contact> onCreateLoader(int id, Bundle args) {
             Uri lookupUri = args.getParcelable(LOADER_ARG_CONTACT_URI);
             return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */,
                     true /* loadStreamItems */, true /* load invitable account types */,
@@ -190,7 +191,7 @@
         }
 
         @Override
-        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
+        public void onLoadFinished(Loader<Contact> loader, Contact data) {
             if (!mLookupUri.equals(data.getRequestedUri())) {
                 Log.e(TAG, "Different URI: requested=" + mLookupUri + "  actual=" + data);
                 return;
@@ -219,7 +220,7 @@
         }
 
         @Override
-        public void onLoaderReset(Loader<ContactLoader.Result> loader) {}
+        public void onLoaderReset(Loader<Contact> loader) {}
     };
 
     @Override
@@ -461,14 +462,14 @@
 
     /** Toggles whether to load stream items. Just for debugging */
     public void toggleLoadStreamItems() {
-        Loader<ContactLoader.Result> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
+        Loader<Contact> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
         ContactLoader loader = (ContactLoader) loaderObj;
         loader.setLoadStreamItems(!loader.getLoadStreamItems());
     }
 
     /** Returns whether to load stream items. Just for debugging */
     public boolean getLoadStreamItems() {
-        Loader<ContactLoader.Result> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
+        Loader<Contact> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
         ContactLoader loader = (ContactLoader) loaderObj;
         return loader != null && loader.getLoadStreamItems();
     }
diff --git a/src/com/android/contacts/detail/PhotoSelectionHandler.java b/src/com/android/contacts/detail/PhotoSelectionHandler.java
index 569012e..b5e406a 100644
--- a/src/com/android/contacts/detail/PhotoSelectionHandler.java
+++ b/src/com/android/contacts/detail/PhotoSelectionHandler.java
@@ -39,12 +39,12 @@
 
 import com.android.contacts.R;
 import com.android.contacts.editor.PhotoActionPopup;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityDeltaList;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.RawContactDeltaList;
 import com.android.contacts.util.ContactPhotoUtils;
 
 import java.io.File;
@@ -64,12 +64,12 @@
     private final View mPhotoView;
     private final int mPhotoMode;
     private final int mPhotoPickSize;
-    private final EntityDeltaList mState;
+    private final RawContactDeltaList mState;
     private final boolean mIsDirectoryContact;
     private ListPopupWindow mPopup;
 
     public PhotoSelectionHandler(Context context, View photoView, int photoMode,
-            boolean isDirectoryContact, EntityDeltaList state) {
+            boolean isDirectoryContact, RawContactDeltaList state) {
         mContext = context;
         mPhotoView = photoView;
         mPhotoMode = photoMode;
@@ -162,12 +162,12 @@
      *     or null if the photo could not be parsed or none of the accounts associated with the
      *     contact are writable.
      */
-    public EntityDeltaList getDeltaForAttachingPhotoToContact() {
+    public RawContactDeltaList getDeltaForAttachingPhotoToContact() {
         // Find the first writable entity.
         int writableEntityIndex = getWritableEntityIndex();
         if (writableEntityIndex != -1) {
             // We are guaranteed to have contact data if we have a writable entity index.
-            final EntityDelta delta = mState.get(writableEntityIndex);
+            final RawContactDelta delta = mState.get(writableEntityIndex);
 
             // Need to find the right account so that EntityModifier knows which fields to add
             final ContentValues entityValues = delta.getValues().getCompleteValues();
@@ -176,10 +176,10 @@
             final AccountType accountType = AccountTypeManager.getInstance(mContext).getAccountType(
                         type, dataSet);
 
-            final ValuesDelta child = EntityModifier.ensureKindExists(
+            final ValuesDelta child = RawContactModifier.ensureKindExists(
                     delta, accountType, Photo.CONTENT_ITEM_TYPE);
             child.setFromTemplate(false);
-            child.put(Photo.IS_SUPER_PRIMARY, 1);
+            child.setSuperPrimary(true);
 
             return mState;
         }
diff --git a/src/com/android/contacts/detail/StreamItemAdapter.java b/src/com/android/contacts/detail/StreamItemAdapter.java
index 7564167..15219cd 100644
--- a/src/com/android/contacts/detail/StreamItemAdapter.java
+++ b/src/com/android/contacts/detail/StreamItemAdapter.java
@@ -23,8 +23,8 @@
 import android.widget.BaseAdapter;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 import com.android.contacts.util.StreamItemEntry;
 import com.google.common.collect.Lists;
 
diff --git a/src/com/android/contacts/editor/AggregationSuggestionEngine.java b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
index 7c0e668..0da05e8 100644
--- a/src/com/android/contacts/editor/AggregationSuggestionEngine.java
+++ b/src/com/android/contacts/editor/AggregationSuggestionEngine.java
@@ -37,7 +37,7 @@
 import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
diff --git a/src/com/android/contacts/editor/AggregationSuggestionView.java b/src/com/android/contacts/editor/AggregationSuggestionView.java
index 77b678a..7327a4c 100644
--- a/src/com/android/contacts/editor/AggregationSuggestionView.java
+++ b/src/com/android/contacts/editor/AggregationSuggestionView.java
@@ -28,8 +28,8 @@
 import com.android.contacts.R;
 import com.android.contacts.editor.AggregationSuggestionEngine.RawContact;
 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
diff --git a/src/com/android/contacts/editor/BaseRawContactEditorView.java b/src/com/android/contacts/editor/BaseRawContactEditorView.java
index 65395a8..66fc864 100644
--- a/src/com/android/contacts/editor/BaseRawContactEditorView.java
+++ b/src/com/android/contacts/editor/BaseRawContactEditorView.java
@@ -17,7 +17,6 @@
 package com.android.contacts.editor;
 
 import android.content.Context;
-import android.content.Entity;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
@@ -29,20 +28,20 @@
 import android.widget.LinearLayout;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
-import com.android.contacts.model.AccountType.EditType;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountType.EditType;
 
 /**
  * Base view that provides common code for the editor interaction for a specific
- * RawContact represented through an {@link EntityDelta}.
+ * RawContact represented through an {@link RawContactDelta}.
  * <p>
  * Internal updates are performed against {@link ValuesDelta} so that the
- * source {@link Entity} can be swapped out. Any state-based changes, such as
+ * source {@link RawContact} 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 AccountType} are enforced.
+ * {@link RawContactModifier} to ensure that {@link AccountType} are enforced.
  */
 public abstract class BaseRawContactEditorView extends LinearLayout {
 
@@ -78,7 +77,7 @@
 
     /**
      * Assign the given {@link Bitmap} to the internal {@link PhotoEditorView}
-     * for the {@link EntityDelta} currently being edited.
+     * for the {@link RawContactDelta} currently being edited.
      */
     public void setPhotoBitmap(Bitmap bitmap) {
         mPhoto.setPhotoBitmap(bitmap);
@@ -115,10 +114,10 @@
 
     /**
      * Set the internal state for this view, given a current
-     * {@link EntityDelta} state and the {@link AccountType} that
+     * {@link RawContactDelta} state and the {@link AccountType} that
      * apply to that state.
      */
-    public abstract void setState(EntityDelta state, AccountType source, ViewIdGenerator vig,
+    public abstract void setState(RawContactDelta state, AccountType source, ViewIdGenerator vig,
             boolean isProfile);
 
     /* package */ void setExpanded(boolean value) {
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 536c41d..780279e 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -29,7 +29,6 @@
 import android.content.Context;
 import android.content.CursorLoader;
 import android.content.DialogInterface;
-import android.content.Entity;
 import android.content.Intent;
 import android.content.Loader;
 import android.database.Cursor;
@@ -62,7 +61,6 @@
 import android.widget.ListPopupWindow;
 import android.widget.Toast;
 
-import com.android.contacts.ContactLoader;
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
@@ -72,18 +70,22 @@
 import com.android.contacts.detail.PhotoSelectionHandler;
 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
 import com.android.contacts.editor.Editor.EditorListener;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityDeltaList;
-import com.android.contacts.model.EntityModifier;
-import com.android.contacts.model.GoogleAccountType;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.ContactLoader;
+import com.android.contacts.model.RawContact;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDeltaList;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
+import com.android.contacts.model.account.GoogleAccountType;
 import com.android.contacts.util.AccountsListAdapter;
 import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.util.ContactPhotoUtils;
 import com.android.contacts.util.HelpUtils;
+import com.google.common.collect.ImmutableList;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -224,7 +226,7 @@
     private ContactEditorUtils mEditorUtils;
 
     private LinearLayout mContent;
-    private EntityDeltaList mState;
+    private RawContactDeltaList mState;
 
     private ViewIdGenerator mViewIdGenerator;
 
@@ -420,7 +422,7 @@
             mViewIdGenerator = new ViewIdGenerator();
         } else {
             // Read state from savedState. No loading involved here
-            mState = savedState.<EntityDeltaList> getParcelable(KEY_EDIT_STATE);
+            mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
             mRawContactIdRequestingPhoto = savedState.getLong(
                     KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
             mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
@@ -436,7 +438,7 @@
         }
     }
 
-    public void setData(ContactLoader.Result data) {
+    public void setData(Contact data) {
         // If we have already loaded data, we do not want to change it here to not confuse the user
         if (mState != null) {
             Log.v(TAG, "Ignoring background change. This will have to be rebased later");
@@ -444,19 +446,17 @@
         }
 
         // See if this edit operation needs to be redirected to a custom editor
-        ArrayList<Entity> entities = data.getEntities();
-        if (entities.size() == 1) {
-            Entity entity = entities.get(0);
-            ContentValues entityValues = entity.getEntityValues();
-            String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
-            String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
-            AccountType accountType = AccountTypeManager.getInstance(mContext).getAccountType(
-                    type, dataSet);
+        ImmutableList<RawContact> rawContacts = data.getRawContacts();
+        if (rawContacts.size() == 1) {
+            RawContact rawContact = rawContacts.get(0);
+            String type = rawContact.getAccountTypeString();
+            String dataSet = rawContact.getDataSet();
+            AccountType accountType = rawContact.getAccountType();
             if (accountType.getEditContactActivityClassName() != null &&
                     !accountType.areContactsWritable()) {
                 if (mListener != null) {
-                    String name = entityValues.getAsString(RawContacts.ACCOUNT_NAME);
-                    long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID);
+                    String name = rawContact.getAccountName();
+                    long rawContactId = rawContact.getId();
                     mListener.onCustomEditContactActivityRequested(
                             new AccountWithDataSet(name, type, dataSet),
                             ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
@@ -474,10 +474,10 @@
         mListener.onCustomEditContactActivityRequested(account, uri, null, false);
     }
 
-    private void bindEditorsForExistingContact(ContactLoader.Result contact) {
+    private void bindEditorsForExistingContact(Contact contact) {
         setEnabled(true);
 
-        mState = contact.createEntityDeltaList();
+        mState = contact.createRawContactDeltaList();
         setIntentExtras(mIntentExtras);
         mIntentExtras = null;
 
@@ -486,7 +486,7 @@
         boolean localProfileExists = false;
 
         if (mIsUserProfile) {
-            for (EntityDelta state : mState) {
+            for (RawContactDelta state : mState) {
                 // For profile contacts, we need a different query URI
                 state.setProfileQueryUri();
                 // Try to find a local profile contact
@@ -496,11 +496,11 @@
             }
             // Editor should always present a local profile for editing
             if (!localProfileExists) {
-                final ContentValues values = new ContentValues();
-                values.putNull(RawContacts.ACCOUNT_NAME);
-                values.putNull(RawContacts.ACCOUNT_TYPE);
-                values.putNull(RawContacts.DATA_SET);
-                EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
+                final RawContact rawContact = new RawContact(mContext);
+                rawContact.setAccountToLocal();
+
+                RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter(
+                        rawContact.getValues()));
                 insert.setProfileQueryUri();
                 mState.add(insert);
             }
@@ -519,13 +519,11 @@
         }
 
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-        for (EntityDelta state : mState) {
-            final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = state.getValues().getAsString(RawContacts.DATA_SET);
-            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+        for (RawContactDelta state : mState) {
+            final AccountType type = state.getAccountType(accountTypes);
             if (type.areContactsWritable()) {
                 // Apply extras to the first writable raw contact only
-                EntityModifier.parseExtras(mContext, type, state, extras);
+                RawContactModifier.parseExtras(mContext, type, state, extras);
                 break;
             }
         }
@@ -604,7 +602,8 @@
      * @param newAccount New account to be used.
      */
     private void rebindEditorsForNewContact(
-            EntityDelta oldState, AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
+            RawContactDelta oldState, AccountWithDataSet oldAccount,
+            AccountWithDataSet newAccount) {
         AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
         AccountType oldAccountType = accountTypes.getAccountType(
                 oldAccount.type, oldAccount.dataSet);
@@ -628,36 +627,34 @@
     }
 
     private void bindEditorsForNewContact(AccountWithDataSet newAccount,
-            final AccountType newAccountType, EntityDelta oldState, AccountType oldAccountType) {
+            final AccountType newAccountType, RawContactDelta oldState,
+            AccountType oldAccountType) {
         mStatus = Status.EDITING;
 
-        final ContentValues values = new ContentValues();
+        final RawContact rawContact = new RawContact(mContext);
         if (newAccount != null) {
-            values.put(RawContacts.ACCOUNT_NAME, newAccount.name);
-            values.put(RawContacts.ACCOUNT_TYPE, newAccount.type);
-            values.put(RawContacts.DATA_SET, newAccount.dataSet);
+            rawContact.setAccount(newAccount);
         } else {
-            values.putNull(RawContacts.ACCOUNT_NAME);
-            values.putNull(RawContacts.ACCOUNT_TYPE);
-            values.putNull(RawContacts.DATA_SET);
+            rawContact.setAccountToLocal();
         }
 
-        EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
+        RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter(rawContact.getValues()));
         if (oldState == null) {
             // Parse any values from incoming intent
-            EntityModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras);
+            RawContactModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras);
         } else {
-            EntityModifier.migrateStateForNewContact(mContext, oldState, insert,
+            RawContactModifier.migrateStateForNewContact(mContext, oldState, insert,
                     oldAccountType, newAccountType);
         }
 
         // Ensure we have some default fields (if the account type does not support a field,
         // ensureKind will not add it, so it is safe to add e.g. Event)
-        EntityModifier.ensureKindExists(insert, newAccountType, Phone.CONTENT_ITEM_TYPE);
-        EntityModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE);
-        EntityModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE);
-        EntityModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE);
-        EntityModifier.ensureKindExists(insert, newAccountType, StructuredPostal.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(insert, newAccountType, Phone.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(insert, newAccountType,
+                StructuredPostal.CONTENT_ITEM_TYPE);
 
         // Set the correct URI for saving the contact as a profile
         if (mNewLocalProfile) {
@@ -666,7 +663,7 @@
 
         if (mState == null) {
             // Create state if none exists yet
-            mState = EntityDeltaList.fromSingle(insert);
+            mState = RawContactDeltaList.fromSingle(insert);
         } else {
             // Add contact onto end of existing state
             mState.add(insert);
@@ -690,14 +687,11 @@
         int numRawContacts = mState.size();
         for (int i = 0; i < numRawContacts; i++) {
             // TODO ensure proper ordering of entities in the list
-            final EntityDelta entity = mState.get(i);
-            final ValuesDelta values = entity.getValues();
-            if (!values.isVisible()) continue;
+            final RawContactDelta rawContactDelta = mState.get(i);
+            if (!rawContactDelta.isVisible()) continue;
 
-            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = values.getAsString(RawContacts.DATA_SET);
-            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
-            final long rawContactId = values.getAsLong(RawContacts._ID);
+            final AccountType type = rawContactDelta.getAccountType(accountTypes);
+            final long rawContactId = rawContactDelta.getRawContactId();
 
             final BaseRawContactEditorView editor;
             if (!type.areContactsWritable()) {
@@ -724,7 +718,7 @@
 
             mContent.addView(editor);
 
-            editor.setState(entity, type, mViewIdGenerator, isEditingUserProfile());
+            editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
 
             // Set up the photo handler.
             bindPhotoHandler(editor, type, mState);
@@ -797,7 +791,7 @@
     }
 
     private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type,
-            EntityDeltaList state) {
+            RawContactDeltaList state) {
         final int mode;
         if (type.areContactsWritable()) {
             if (editor.hasSetPhoto()) {
@@ -852,11 +846,10 @@
         // Find the associated account for this contact (retrieve it here because there are
         // multiple paths to creating a contact and this ensures we always have the correct
         // account).
-        final EntityDelta entity = mState.get(0);
-        final ValuesDelta values = entity.getValues();
-        String name = values.getAsString(RawContacts.ACCOUNT_NAME);
-        String type = values.getAsString(RawContacts.ACCOUNT_TYPE);
-        String dataSet = values.getAsString(RawContacts.DATA_SET);
+        final RawContactDelta rawContactDelta = mState.get(0);
+        String name = rawContactDelta.getAccountName();
+        String type = rawContactDelta.getAccountType();
+        String dataSet = rawContactDelta.getDataSet();
 
         AccountWithDataSet account = (name == null || type == null) ? null :
                 new AccountWithDataSet(name, type, dataSet);
@@ -864,12 +857,11 @@
     }
 
     private void addAccountSwitcher(
-            final EntityDelta currentState, BaseRawContactEditorView editor) {
-        ValuesDelta values = currentState.getValues();
+            final RawContactDelta currentState, BaseRawContactEditorView editor) {
         final AccountWithDataSet currentAccount = new AccountWithDataSet(
-                values.getAsString(RawContacts.ACCOUNT_NAME),
-                values.getAsString(RawContacts.ACCOUNT_TYPE),
-                values.getAsString(RawContacts.DATA_SET));
+                currentState.getAccountName(),
+                currentState.getAccountType(),
+                currentState.getDataSet());
         final View accountView = editor.findViewById(R.id.account);
         final View anchorView = editor.findViewById(R.id.account_container);
         accountView.setOnClickListener(new View.OnClickListener() {
@@ -1005,7 +997,7 @@
      */
     private boolean hasPendingChanges() {
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
-        return EntityModifier.hasChanges(mState, accountTypes);
+        return RawContactModifier.hasChanges(mState, accountTypes);
     }
 
     /**
@@ -1209,10 +1201,8 @@
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
         int size = mState.size();
         for (int i = 0; i < size; i++) {
-            ValuesDelta values = mState.get(i).getValues();
-            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = values.getAsString(RawContacts.DATA_SET);
-            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+            RawContactDelta entity = mState.get(i);
+            final AccountType type = entity.getAccountType(accountTypes);
             if (type.areContactsWritable()) {
                 return true;
             }
@@ -1273,12 +1263,12 @@
                 Bundle intentExtras, boolean redirect);
     }
 
-    private class EntityDeltaComparator implements Comparator<EntityDelta> {
+    private class EntityDeltaComparator implements Comparator<RawContactDelta> {
         /**
          * Compare EntityDeltas for sorting the stack of editors.
          */
         @Override
-        public int compare(EntityDelta one, EntityDelta two) {
+        public int compare(RawContactDelta one, RawContactDelta two) {
             // Check direct equality
             if (one.equals(two)) {
                 return 0;
@@ -1333,11 +1323,9 @@
             }
 
             // Check account name
-            ValuesDelta oneValues = one.getValues();
-            String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME);
+            String oneAccount = one.getAccountName();
             if (oneAccount == null) oneAccount = "";
-            ValuesDelta twoValues = two.getValues();
-            String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME);
+            String twoAccount = two.getAccountName();
             if (twoAccount == null) twoAccount = "";
             value = oneAccount.compareTo(twoAccount);
             if (value != 0) {
@@ -1345,8 +1333,8 @@
             }
 
             // Both are in the same account, fall back to contact ID
-            Long oneId = oneValues.getAsLong(RawContacts._ID);
-            Long twoId = twoValues.getAsLong(RawContacts._ID);
+            Long oneId = one.getRawContactId();
+            Long twoId = two.getRawContactId();
             if (oneId == null) {
                 return -1;
             } else if (twoId == null) {
@@ -1362,7 +1350,7 @@
      */
     protected long getContactId() {
         if (mState != null) {
-            for (EntityDelta rawContact : mState) {
+            for (RawContactDelta rawContact : mState) {
                 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
                 if (contactId != null) {
                     return contactId;
@@ -1662,14 +1650,13 @@
         int countWithPicture = 0;
         final int numEntities = mState.size();
         for (int i = 0; i < numEntities; i++) {
-            final EntityDelta entity = mState.get(i);
-            final ValuesDelta values = entity.getValues();
-            if (values.isVisible()) {
+            final RawContactDelta entity = mState.get(i);
+            if (entity.isVisible()) {
                 final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
-                if (primary != null && primary.getAsByteArray(Photo.PHOTO) != null) {
+                if (primary != null && primary.getPhoto() != null) {
                     countWithPicture++;
                 } else {
-                    final long rawContactId = values.getAsLong(RawContacts._ID);
+                    final long rawContactId = entity.getRawContactId();
                     final String path = mUpdatedPhotos.getString(String.valueOf(rawContactId));
                     if (path != null) {
                         final File file = new File(path);
@@ -1690,16 +1677,16 @@
     /**
      * The listener for the data loader
      */
-    private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDataLoaderListener =
-            new LoaderCallbacks<ContactLoader.Result>() {
+    private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener =
+            new LoaderCallbacks<Contact>() {
         @Override
-        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+        public Loader<Contact> onCreateLoader(int id, Bundle args) {
             mLoaderStartTime = SystemClock.elapsedRealtime();
             return new ContactLoader(mContext, mLookupUri, true);
         }
 
         @Override
-        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
+        public void onLoadFinished(Loader<Contact> loader, Contact data) {
             final long loaderCurrentTime = SystemClock.elapsedRealtime();
             Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
             if (!data.isLoaded()) {
@@ -1719,7 +1706,7 @@
         }
 
         @Override
-        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
+        public void onLoaderReset(Loader<Contact> loader) {
         }
     };
 
@@ -1772,7 +1759,7 @@
         private final PhotoActionListener mPhotoEditorListener;
 
         public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode,
-                EntityDeltaList state) {
+                RawContactDeltaList state) {
             super(context, editor.getPhotoEditor(), photoMode, false, state);
             mEditor = editor;
             mRawContactId = editor.getRawContactId();
diff --git a/src/com/android/contacts/editor/ContactEditorUtils.java b/src/com/android/contacts/editor/ContactEditorUtils.java
index 0029b14..2791c2d 100644
--- a/src/com/android/contacts/editor/ContactEditorUtils.java
+++ b/src/com/android/contacts/editor/ContactEditorUtils.java
@@ -26,9 +26,9 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.test.NeededForTesting;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
diff --git a/src/com/android/contacts/editor/Editor.java b/src/com/android/contacts/editor/Editor.java
index 025f092..05336f2 100644
--- a/src/com/android/contacts/editor/Editor.java
+++ b/src/com/android/contacts/editor/Editor.java
@@ -18,9 +18,9 @@
 
 import android.provider.ContactsContract.Data;
 
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.dataitem.DataKind;
 
 /**
  * Generic definition of something that edits a {@link Data} row through an
@@ -60,7 +60,7 @@
      * builds any needed views. Any changes performed by the user will be
      * written back to that same object.
      */
-    public void setValues(DataKind kind, ValuesDelta values, EntityDelta state, boolean readOnly,
+    public void setValues(DataKind kind, ValuesDelta values, RawContactDelta state, boolean readOnly,
             ViewIdGenerator vig);
 
     public void setDeletable(boolean deletable);
diff --git a/src/com/android/contacts/editor/EventFieldEditorView.java b/src/com/android/contacts/editor/EventFieldEditorView.java
index ff2622f..ca1bf64 100644
--- a/src/com/android/contacts/editor/EventFieldEditorView.java
+++ b/src/com/android/contacts/editor/EventFieldEditorView.java
@@ -29,11 +29,11 @@
 import com.android.contacts.datepicker.DatePicker;
 import com.android.contacts.datepicker.DatePickerDialog;
 import com.android.contacts.datepicker.DatePickerDialog.OnDateSetListener;
-import com.android.contacts.model.AccountType.EditField;
-import com.android.contacts.model.AccountType.EventEditType;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.account.AccountType.EditField;
+import com.android.contacts.model.account.AccountType.EventEditType;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.DateUtils;
 
 import java.text.ParsePosition;
@@ -108,7 +108,7 @@
     }
 
     @Override
-    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly,
+    public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,
             ViewIdGenerator vig) {
         if (kind.fieldList.size() != 1) throw new IllegalStateException("kind must have 1 field");
         super.setValues(kind, entry, state, readOnly, vig);
diff --git a/src/com/android/contacts/editor/GroupMembershipView.java b/src/com/android/contacts/editor/GroupMembershipView.java
index f405e4d..b84e22b 100644
--- a/src/com/android/contacts/editor/GroupMembershipView.java
+++ b/src/com/android/contacts/editor/GroupMembershipView.java
@@ -21,7 +21,6 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
-import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
@@ -40,10 +39,10 @@
 import com.android.contacts.R;
 import com.android.contacts.interactions.GroupCreationDialogFragment;
 import com.android.contacts.interactions.GroupCreationDialogFragment.OnGroupCreatedListener;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.internal.util.Objects;
 
 import java.util.ArrayList;
@@ -127,7 +126,7 @@
         }
     }
 
-    private EntityDelta mState;
+    private RawContactDelta mState;
     private Cursor mGroupMetaData;
     private String mAccountName;
     private String mAccountType;
@@ -197,12 +196,11 @@
         }
     }
 
-    public void setState(EntityDelta state) {
+    public void setState(RawContactDelta state) {
         mState = state;
-        ValuesDelta values = state.getValues();
-        mAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
-        mAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
-        mDataSet = values.getAsString(RawContacts.DATA_SET);
+        mAccountType = mState.getAccountType();
+        mAccountName = mState.getAccountName();
+        mDataSet = mState.getDataSet();
         mDefaultGroupVisibilityKnown = false;
         mCreatedNewGroup = false;
         updateView();
@@ -357,7 +355,7 @@
         if (entries != null) {
             for (ValuesDelta entry : entries) {
                 if (!entry.isDelete()) {
-                    Long groupId = entry.getAsLong(GroupMembership.GROUP_ROW_ID);
+                    Long groupId = entry.getGroupRowId();
                     if (groupId != null && groupId != mFavoritesGroupId
                             && (groupId != mDefaultGroupId || mDefaultGroupVisible)
                             && !isGroupChecked(groupId)) {
@@ -372,8 +370,8 @@
             GroupSelectionItem item = mAdapter.getItem(i);
             long groupId = item.getGroupId();
             if (item.isChecked() && !hasMembership(groupId)) {
-                ValuesDelta entry = EntityModifier.insertChild(mState, mKind);
-                entry.put(GroupMembership.GROUP_ROW_ID, groupId);
+                ValuesDelta entry = RawContactModifier.insertChild(mState, mKind);
+                entry.setGroupRowId(groupId);
             }
         }
 
@@ -400,7 +398,7 @@
         if (entries != null) {
             for (ValuesDelta values : entries) {
                 if (!values.isDelete()) {
-                    Long id = values.getAsLong(GroupMembership.GROUP_ROW_ID);
+                    Long id = values.getGroupRowId();
                     if (id != null && id == groupId) {
                         return true;
                     }
diff --git a/src/com/android/contacts/editor/KindSectionView.java b/src/com/android/contacts/editor/KindSectionView.java
index ae0262a..2d4263b 100644
--- a/src/com/android/contacts/editor/KindSectionView.java
+++ b/src/com/android/contacts/editor/KindSectionView.java
@@ -28,10 +28,10 @@
 
 import com.android.contacts.R;
 import com.android.contacts.editor.Editor.EditorListener;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.dataitem.DataKind;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -50,7 +50,7 @@
     private String mTitleString;
 
     private DataKind mKind;
-    private EntityDelta mState;
+    private RawContactDelta mState;
     private boolean mReadOnly;
 
     private ViewIdGenerator mViewIdGenerator;
@@ -136,7 +136,7 @@
         }
     }
 
-    public void setState(DataKind kind, EntityDelta state, boolean readOnly, ViewIdGenerator vig) {
+    public void setState(DataKind kind, RawContactDelta state, boolean readOnly, ViewIdGenerator vig) {
         mKind = kind;
         mState = state;
         mReadOnly = readOnly;
@@ -237,7 +237,7 @@
             updateEmptyEditors();
             // If there are no existing empty editors and it's possible to add
             // another field, then make the "add footer" field visible.
-            if (!hasEmptyEditor() && EntityModifier.canInsert(mState, mKind)) {
+            if (!hasEmptyEditor() && RawContactModifier.canInsert(mState, mKind)) {
                 if (animate) {
                     EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter);
                 } else {
@@ -364,7 +364,7 @@
 
         // Insert a new child, create its view and set its focus
         if (values == null) {
-            values = EntityModifier.insertChild(mState, mKind);
+            values = RawContactModifier.insertChild(mState, mKind);
         }
 
         final View newField = createEditorView(values);
diff --git a/src/com/android/contacts/editor/LabeledEditorView.java b/src/com/android/contacts/editor/LabeledEditorView.java
index 288e915..789f425 100644
--- a/src/com/android/contacts/editor/LabeledEditorView.java
+++ b/src/com/android/contacts/editor/LabeledEditorView.java
@@ -21,7 +21,6 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnShowListener;
-import android.content.Entity;
 import android.os.Bundle;
 import android.os.Handler;
 import android.text.Editable;
@@ -47,11 +46,11 @@
 
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType.EditType;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.account.AccountType.EditType;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.DialogManager;
 import com.android.contacts.util.DialogManager.DialogShowingView;
 
@@ -59,7 +58,7 @@
 
 /**
  * Base class for editors that handles labels and values. Uses
- * {@link ValuesDelta} to read any existing {@link Entity} values, and to
+ * {@link ValuesDelta} to read any existing {@link RawContact} values, and to
  * correctly write any changes values.
  */
 public abstract class LabeledEditorView extends LinearLayout implements Editor, DialogShowingView {
@@ -76,7 +75,7 @@
 
     private DataKind mKind;
     private ValuesDelta mEntry;
-    private EntityDelta mState;
+    private RawContactDelta mState;
     private boolean mReadOnly;
     private boolean mWasEmpty = true;
     private boolean mIsDeletable = true;
@@ -342,7 +341,7 @@
      * structure and {@link ValuesDelta} describing the content to edit.
      */
     @Override
-    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly,
+    public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,
             ViewIdGenerator vig) {
         mKind = kind;
         mEntry = entry;
@@ -359,11 +358,11 @@
         setVisibility(View.VISIBLE);
 
         // Display label selector if multiple types available
-        final boolean hasTypes = EntityModifier.hasEditTypes(kind);
+        final boolean hasTypes = RawContactModifier.hasEditTypes(kind);
         setupLabelButton(hasTypes);
         mLabel.setEnabled(!readOnly && isEnabled());
         if (hasTypes) {
-            mType = EntityModifier.getCurrentType(entry, kind);
+            mType = RawContactModifier.getCurrentType(entry, kind);
             rebuildLabel();
         }
     }
@@ -398,7 +397,7 @@
                 final String customText = editText.getText().toString().trim();
                 if (ContactsUtils.isGraphic(customText)) {
                     final List<EditType> allTypes =
-                            EntityModifier.getValidTypes(mState, mKind, null);
+                            RawContactModifier.getValidTypes(mState, mKind, null);
                     mType = null;
                     for (EditType editType : allTypes) {
                         if (editType.customColumn != null) {
@@ -534,7 +533,7 @@
                 }
             }
 
-            addAll(EntityModifier.getValidTypes(mState, mKind, mType));
+            addAll(RawContactModifier.getValidTypes(mState, mKind, mType));
         }
 
         public boolean hasCustomSelection() {
diff --git a/src/com/android/contacts/editor/PhoneticNameEditorView.java b/src/com/android/contacts/editor/PhoneticNameEditorView.java
index 1700817..8cbb921 100644
--- a/src/com/android/contacts/editor/PhoneticNameEditorView.java
+++ b/src/com/android/contacts/editor/PhoneticNameEditorView.java
@@ -16,15 +16,15 @@
 
 package com.android.contacts.editor;
 
-import android.content.ContentValues;
 import android.content.Context;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.dataitem.DataKind;
+import com.android.contacts.model.dataitem.StructuredNameDataItem;
 
 /**
  * A dedicated editor for phonetic name. It is similar to {@link StructuredNameEditorView}.
@@ -61,19 +61,16 @@
         }
 
         private void parsePhoneticName(String value) {
-            ContentValues values = PhoneticNameEditorView.parsePhoneticName(value, null);
-            mValues.put(StructuredName.PHONETIC_FAMILY_NAME,
-                    values.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
-            mValues.put(StructuredName.PHONETIC_MIDDLE_NAME,
-                    values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME));
-            mValues.put(StructuredName.PHONETIC_GIVEN_NAME,
-                    values.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
+            StructuredNameDataItem dataItem = PhoneticNameEditorView.parsePhoneticName(value, null);
+            mValues.setPhoneticFamilyName(dataItem.getPhoneticFamilyName());
+            mValues.setPhoneticMiddleName(dataItem.getPhoneticMiddleName());
+            mValues.setPhoneticGivenName(dataItem.getPhoneticGivenName());
         }
 
         private void buildPhoneticName() {
-            String family = mValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
-            String middle = mValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
-            String given = mValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+            String family = mValues.getPhoneticFamilyName();
+            String middle = mValues.getPhoneticMiddleName();
+            String given = mValues.getPhoneticGivenName();
             mPhoneticName = PhoneticNameEditorView.buildPhoneticName(family, middle, given);
         }
 
@@ -100,7 +97,8 @@
      * created.
      * @return ContentValues with parsed data. Those data can be null.
      */
-    public static ContentValues parsePhoneticName(String phoneticName, ContentValues values) {
+    public static StructuredNameDataItem parsePhoneticName(String phoneticName,
+            StructuredNameDataItem item) {
         String family = null;
         String middle = null;
         String given = null;
@@ -123,13 +121,13 @@
             }
         }
 
-        if (values == null) {
-            values = new ContentValues();
+        if (item == null) {
+            item = new StructuredNameDataItem();
         }
-        values.put(StructuredName.PHONETIC_FAMILY_NAME, family);
-        values.put(StructuredName.PHONETIC_MIDDLE_NAME, middle);
-        values.put(StructuredName.PHONETIC_GIVEN_NAME, given);
-        return values;
+        item.setPhoneticFamilyName(family);
+        item.setPhoneticMiddleName(middle);
+        item.setPhoneticGivenName(given);
+        return item;
     }
 
     /**
@@ -172,7 +170,7 @@
     }
 
     @Override
-    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly,
+    public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,
             ViewIdGenerator vig) {
         if (!(entry instanceof PhoneticValuesDelta)) {
             entry = new PhoneticValuesDelta(entry);
@@ -213,9 +211,9 @@
     public boolean hasData() {
         ValuesDelta entry = getEntry();
 
-        String family = entry.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
-        String middle = entry.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
-        String given = entry.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+        String family = entry.getPhoneticFamilyName();
+        String middle = entry.getPhoneticMiddleName();
+        String given = entry.getPhoneticGivenName();
 
         return !TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle)
                 || !TextUtils.isEmpty(given);
diff --git a/src/com/android/contacts/editor/PhotoEditorView.java b/src/com/android/contacts/editor/PhotoEditorView.java
index 71a4197..30c3bb4 100644
--- a/src/com/android/contacts/editor/PhotoEditorView.java
+++ b/src/com/android/contacts/editor/PhotoEditorView.java
@@ -27,9 +27,9 @@
 
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.ContactPhotoUtils;
 
 /**
@@ -92,7 +92,7 @@
 
     /** {@inheritDoc} */
     @Override
-    public void setValues(DataKind kind, ValuesDelta values, EntityDelta state, boolean readOnly,
+    public void setValues(DataKind kind, ValuesDelta values, RawContactDelta state, boolean readOnly,
             ViewIdGenerator vig) {
         mEntry = values;
         mReadOnly = readOnly;
@@ -143,7 +143,7 @@
         mEntry.setFromTemplate(false);
 
         // When the user chooses a new photo mark it as super primary
-        mEntry.put(Photo.IS_SUPER_PRIMARY, 1);
+        mEntry.setSuperPrimary(true);
 
         // Even though high-res photos cannot be saved by passing them via
         // an EntityDeltaList (since they cause the Bundle size limit to be
@@ -154,7 +154,7 @@
         final int size = ContactsUtils.getThumbnailSize(getContext());
         final Bitmap scaled = Bitmap.createScaledBitmap(photo, size, size, false);
         final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled);
-        if (compressed != null) mEntry.put(Photo.PHOTO, compressed);
+        if (compressed != null) mEntry.setPhoto(compressed);
     }
 
     /**
diff --git a/src/com/android/contacts/editor/RawContactEditorView.java b/src/com/android/contacts/editor/RawContactEditorView.java
index 51ba400..daccdd8 100644
--- a/src/com/android/contacts/editor/RawContactEditorView.java
+++ b/src/com/android/contacts/editor/RawContactEditorView.java
@@ -17,7 +17,6 @@
 package com.android.contacts.editor;
 
 import android.content.Context;
-import android.content.Entity;
 import android.database.Cursor;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
@@ -25,7 +24,6 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -40,26 +38,26 @@
 
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
-import com.android.contacts.model.AccountType.EditType;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountType.EditType;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.internal.util.Objects;
 
 import java.util.ArrayList;
 
 /**
  * Custom view that provides all the editor interaction for a specific
- * {@link Contacts} represented through an {@link EntityDelta}. Callers can
+ * {@link Contacts} represented through an {@link RawContactDelta}. Callers can
  * reuse this view and quickly rebuild its contents through
- * {@link #setState(EntityDelta, AccountType, ViewIdGenerator)}.
+ * {@link #setState(RawContactDelta, AccountType, ViewIdGenerator)}.
  * <p>
  * Internal updates are performed against {@link ValuesDelta} so that the
- * source {@link Entity} can be swapped out. Any state-based changes, such as
+ * source {@link RawContact} 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 AccountType} are enforced.
+ * {@link RawContactModifier} to ensure that {@link AccountType} are enforced.
  */
 public class RawContactEditorView extends BaseRawContactEditorView {
     private LayoutInflater mInflater;
@@ -80,7 +78,7 @@
     private boolean mAutoAddToDefaultGroup = true;
     private Cursor mGroupMetaData;
     private DataKind mGroupMembershipKind;
-    private EntityDelta mState;
+    private RawContactDelta mState;
 
     private boolean mPhoneticNameAdded;
 
@@ -152,11 +150,11 @@
 
     /**
      * Set the internal state for this view, given a current
-     * {@link EntityDelta} state and the {@link AccountType} that
+     * {@link RawContactDelta} state and the {@link AccountType} that
      * apply to that state.
      */
     @Override
-    public void setState(EntityDelta state, AccountType type, ViewIdGenerator vig,
+    public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,
             boolean isProfile) {
 
         mState = state;
@@ -170,15 +168,14 @@
         setId(vig.getId(state, null, null, ViewIdGenerator.NO_VIEW_INDEX));
 
         // Make sure we have a StructuredName and Organization
-        EntityModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE);
-        EntityModifier.ensureKindExists(state, type, Organization.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(state, type, Organization.CONTENT_ITEM_TYPE);
 
-        ValuesDelta values = state.getValues();
-        mRawContactId = values.getAsLong(RawContacts._ID);
+        mRawContactId = state.getRawContactId();
 
         // Fill in the account info
         if (isProfile) {
-            String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+            String accountName = state.getAccountName();
             if (TextUtils.isEmpty(accountName)) {
                 mAccountNameTextView.setVisibility(View.GONE);
                 mAccountTypeTextView.setText(R.string.local_profile_title);
@@ -189,7 +186,7 @@
                 mAccountNameTextView.setText(accountName);
             }
         } else {
-            String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+            String accountName = state.getAccountName();
             CharSequence accountType = type.getDisplayLabel(mContext);
             if (TextUtils.isEmpty(accountType)) {
                 accountType = mContext.getString(R.string.account_phone);
@@ -208,7 +205,7 @@
         mAccountIcon.setImageDrawable(type.getDisplayIcon(mContext));
 
         // Show photo editor when supported
-        EntityModifier.ensureKindExists(state, type, Photo.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(state, type, Photo.CONTENT_ITEM_TYPE);
         setHasPhotoEditor((type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null));
         getPhotoEditor().setEnabled(isEnabled());
         mName.setEnabled(isEnabled());
@@ -341,7 +338,7 @@
         ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
         if (entries != null) {
             for (ValuesDelta values : entries) {
-                Long id = values.getAsLong(GroupMembership.GROUP_ROW_ID);
+                Long id = values.getGroupRowId();
                 if (id != null && id.longValue() != 0) {
                     hasGroupMembership = true;
                     break;
@@ -352,8 +349,8 @@
         if (!hasGroupMembership) {
             long defaultGroupId = getDefaultGroupId();
             if (defaultGroupId != -1) {
-                ValuesDelta entry = EntityModifier.insertChild(mState, mGroupMembershipKind);
-                entry.put(GroupMembership.GROUP_ROW_ID, defaultGroupId);
+                ValuesDelta entry = RawContactModifier.insertChild(mState, mGroupMembershipKind);
+                entry.setGroupRowId(defaultGroupId);
             }
         }
     }
@@ -363,9 +360,9 @@
      * account.  Returns -1 if there is no such group.
      */
     private long getDefaultGroupId() {
-        String accountType = mState.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-        String accountName = mState.getValues().getAsString(RawContacts.ACCOUNT_NAME);
-        String accountDataSet = mState.getValues().getAsString(RawContacts.DATA_SET);
+        String accountType = mState.getAccountType();
+        String accountName = mState.getAccountName();
+        String accountDataSet = mState.getDataSet();
         mGroupMetaData.moveToPosition(-1);
         while (mGroupMetaData.moveToNext()) {
             String name = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
diff --git a/src/com/android/contacts/editor/RawContactReadOnlyEditorView.java b/src/com/android/contacts/editor/RawContactReadOnlyEditorView.java
index 25edca9..8e51d18 100644
--- a/src/com/android/contacts/editor/RawContactReadOnlyEditorView.java
+++ b/src/com/android/contacts/editor/RawContactReadOnlyEditorView.java
@@ -39,12 +39,12 @@
 
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
-import com.android.contacts.model.AccountWithDataSet;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.RawContactModifier;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
+import com.android.contacts.model.dataitem.DataKind;
 
 import java.util.ArrayList;
 
@@ -108,11 +108,11 @@
 
     /**
      * Set the internal state for this view, given a current
-     * {@link EntityDelta} state and the {@link AccountType} that
+     * {@link RawContactDelta} state and the {@link AccountType} that
      * apply to that state.
      */
     @Override
-    public void setState(EntityDelta state, AccountType type, ViewIdGenerator vig,
+    public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig,
             boolean isProfile) {
         // Remove any existing sections
         mGeneral.removeAllViews();
@@ -121,13 +121,12 @@
         if (state == null || type == null) return;
 
         // Make sure we have StructuredName
-        EntityModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE);
 
         // Fill in the header info
-        ValuesDelta values = state.getValues();
-        mAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
-        mAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
-        mDataSet = values.getAsString(RawContacts.DATA_SET);
+        mAccountName = state.getAccountName();
+        mAccountType = state.getAccountType();
+        mDataSet = state.getDataSet();
 
         if (isProfile) {
             if (TextUtils.isEmpty(mAccountName)) {
@@ -162,14 +161,14 @@
 
         mAccountIcon.setImageDrawable(type.getDisplayIcon(mContext));
 
-        mRawContactId = values.getAsLong(RawContacts._ID);
+        mRawContactId = state.getRawContactId();
 
         ValuesDelta primary;
 
         // Photo
         DataKind kind = type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE);
         if (kind != null) {
-            EntityModifier.ensureKindExists(state, type, Photo.CONTENT_ITEM_TYPE);
+            RawContactModifier.ensureKindExists(state, type, Photo.CONTENT_ITEM_TYPE);
             boolean hasPhotoEditor = type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null;
             setHasPhotoEditor(hasPhotoEditor);
             primary = state.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
@@ -203,13 +202,13 @@
             for (int i = 0; i < phones.size(); i++) {
                 ValuesDelta phone = phones.get(i);
                 final String phoneNumber = PhoneNumberUtils.formatNumber(
-                        phone.getAsString(Phone.NUMBER),
-                        phone.getAsString(Phone.NORMALIZED_NUMBER),
+                        phone.getPhoneNumber(),
+                        phone.getPhoneNormalizedNumber(),
                         ContactsUtils.getCurrentCountryIso(getContext()));
                 final CharSequence phoneType;
-                if (phone.containsKey(Phone.TYPE)) {
+                if (phone.phoneHasType()) {
                     phoneType = Phone.getTypeLabel(
-                            res, phone.getAsInteger(Phone.TYPE), phone.getAsString(Phone.LABEL));
+                            res, phone.getPhoneType(), phone.getPhoneLabel());
                 } else {
                     phoneType = null;
                 }
@@ -223,11 +222,11 @@
         if (emails != null) {
             for (int i = 0; i < emails.size(); i++) {
                 ValuesDelta email = emails.get(i);
-                final String emailAddress = email.getAsString(Email.DATA);
+                final String emailAddress = email.getEmailData();
                 final CharSequence emailType;
-                if (email.containsKey(Email.TYPE)) {
+                if (email.emailHasType()) {
                     emailType = Email.getTypeLabel(
-                            res, email.getAsInteger(Email.TYPE), email.getAsString(Email.LABEL));
+                            res, email.getEmailType(), email.getEmailLabel());
                 } else {
                     emailType = null;
                 }
diff --git a/src/com/android/contacts/editor/SelectAccountDialogFragment.java b/src/com/android/contacts/editor/SelectAccountDialogFragment.java
index b9b9f7b..3e7ad0e 100644
--- a/src/com/android/contacts/editor/SelectAccountDialogFragment.java
+++ b/src/com/android/contacts/editor/SelectAccountDialogFragment.java
@@ -24,7 +24,7 @@
 import android.content.DialogInterface;
 import android.os.Bundle;
 
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountsListAdapter;
 import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 
diff --git a/src/com/android/contacts/editor/StructuredNameEditorView.java b/src/com/android/contacts/editor/StructuredNameEditorView.java
index ac25f22..3c4476d 100644
--- a/src/com/android/contacts/editor/StructuredNameEditorView.java
+++ b/src/com/android/contacts/editor/StructuredNameEditorView.java
@@ -25,9 +25,11 @@
 import android.text.TextUtils;
 import android.util.AttributeSet;
 
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.dataitem.DataItem;
+import com.android.contacts.model.dataitem.DataKind;
+import com.android.contacts.model.dataitem.StructuredNameDataItem;
 import com.android.contacts.util.NameConverter;
 
 import java.util.HashMap;
@@ -45,7 +47,7 @@
  */
 public class StructuredNameEditorView extends TextFieldsEditorView {
 
-    private ContentValues mSnapshot;
+    private StructuredNameDataItem mSnapshot;
     private boolean mChanged;
 
     public StructuredNameEditorView(Context context) {
@@ -61,11 +63,12 @@
     }
 
     @Override
-    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly,
+    public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,
             ViewIdGenerator vig) {
         super.setValues(kind, entry, state, readOnly, vig);
         if (mSnapshot == null) {
-            mSnapshot = new ContentValues(getValues().getCompleteValues());
+            mSnapshot = (StructuredNameDataItem) DataItem.createFrom(null,
+                    new ContentValues(getValues().getCompleteValues()));
             mChanged = entry.isInsert();
         } else {
             mChanged = false;
@@ -114,12 +117,12 @@
 
         if (!mChanged) {
             for (String field : NameConverter.STRUCTURED_NAME_FIELDS) {
-                values.put(field, mSnapshot.getAsString(field));
+                values.put(field, mSnapshot.getContentValues().getAsString(field));
             }
             return;
         }
 
-        String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
+        String displayName = values.getDisplayName();
         Map<String, String> structuredNameMap = NameConverter.displayNameToStructuredName(
                 getContext(), displayName);
         if (!structuredNameMap.isEmpty()) {
@@ -129,17 +132,16 @@
             }
         }
 
-        mSnapshot.clear();
-        mSnapshot.putAll(values.getCompleteValues());
-        mSnapshot.put(StructuredName.DISPLAY_NAME, displayName);
+        mSnapshot.getContentValues().clear();
+        mSnapshot.getContentValues().putAll(values.getCompleteValues());
+        mSnapshot.setDisplayName(displayName);
     }
 
     private void switchFromStructuredNameToFullName() {
         ValuesDelta values = getValues();
 
         if (!mChanged) {
-            values.put(StructuredName.DISPLAY_NAME,
-                    mSnapshot.getAsString(StructuredName.DISPLAY_NAME));
+            values.setDisplayName(mSnapshot.getDisplayName());
             return;
         }
 
@@ -151,10 +153,10 @@
             values.put(StructuredName.DISPLAY_NAME, displayName);
         }
 
-        mSnapshot.clear();
-        mSnapshot.put(StructuredName.DISPLAY_NAME, values.getAsString(StructuredName.DISPLAY_NAME));
+        mSnapshot.getContentValues().clear();
+        mSnapshot.setDisplayName(values.getDisplayName());
         for (String field : structuredNameMap.keySet()) {
-            mSnapshot.put(field, structuredNameMap.get(field));
+            mSnapshot.getContentValues().put(field, structuredNameMap.get(field));
         }
     }
 
@@ -167,14 +169,14 @@
     }
 
     private void eraseFullName(ValuesDelta values) {
-        values.putNull(StructuredName.DISPLAY_NAME);
+        values.setDisplayName(null);
     }
 
     private void rebuildFullName(ValuesDelta values) {
         Map<String, String> structuredNameMap = valuesToStructuredNameMap(values);
         String displayName = NameConverter.structuredNameToDisplayName(getContext(),
                 structuredNameMap);
-        values.put(StructuredName.DISPLAY_NAME, displayName);
+        values.setDisplayName(displayName);
     }
 
     private void eraseStructuredName(ValuesDelta values) {
@@ -184,7 +186,7 @@
     }
 
     private void rebuildStructuredName(ValuesDelta values) {
-        String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
+        String displayName = values.getDisplayName();
         Map<String, String> structuredNameMap = NameConverter.displayNameToStructuredName(
                 getContext(), displayName);
         for (String field : structuredNameMap.keySet()) {
@@ -202,7 +204,7 @@
     protected Parcelable onSaveInstanceState() {
         SavedState state = new SavedState(super.onSaveInstanceState());
         state.mChanged = mChanged;
-        state.mSnapshot = mSnapshot;
+        state.mSnapshot = mSnapshot.getContentValues();
         return state;
     }
 
@@ -212,7 +214,7 @@
         super.onRestoreInstanceState(ss.mSuperState);
 
         mChanged = ss.mChanged;
-        mSnapshot = ss.mSnapshot;
+        mSnapshot = (StructuredNameDataItem) DataItem.createFrom(null, ss.mSnapshot);
     }
 
     private static class SavedState implements Parcelable {
diff --git a/src/com/android/contacts/editor/TextFieldsEditorView.java b/src/com/android/contacts/editor/TextFieldsEditorView.java
index b558a4b..8558b11 100644
--- a/src/com/android/contacts/editor/TextFieldsEditorView.java
+++ b/src/com/android/contacts/editor/TextFieldsEditorView.java
@@ -17,7 +17,6 @@
 package com.android.contacts.editor;
 
 import android.content.Context;
-import android.content.Entity;
 import android.graphics.Rect;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -38,15 +37,15 @@
 
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType.EditField;
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.account.AccountType.EditField;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.PhoneNumberFormatter;
 
 /**
  * Simple editor that handles labels and any {@link EditField} defined for the
- * entry. Uses {@link ValuesDelta} to read any existing {@link Entity} values,
+ * entry. Uses {@link ValuesDelta} to read any existing {@link RawContact} values,
  * and to correctly write any changes values.
  */
 public class TextFieldsEditorView extends LabeledEditorView {
@@ -171,7 +170,7 @@
     }
 
     @Override
-    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly,
+    public void setValues(DataKind kind, ValuesDelta entry, RawContactDelta state, boolean readOnly,
             ViewIdGenerator vig) {
         super.setValues(kind, entry, state, readOnly, vig);
         // Remove edit texts that we currently have
diff --git a/src/com/android/contacts/editor/ViewIdGenerator.java b/src/com/android/contacts/editor/ViewIdGenerator.java
index 4ab9279..55a42a7 100644
--- a/src/com/android/contacts/editor/ViewIdGenerator.java
+++ b/src/com/android/contacts/editor/ViewIdGenerator.java
@@ -20,9 +20,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import com.android.contacts.model.DataKind;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.dataitem.DataKind;
 
 /**
  * A class that provides unique view ids for {@link ContentEditorView}, {@link KindSectionView},
@@ -62,13 +62,13 @@
     /**
      * Returns an id for a view associated with specified contact field.
      *
-     * @param entity {@link EntityDelta} associated with the view
+     * @param entity {@link RawContactDelta} associated with the view
      * @param kind {@link DataKind} associated with the view, or null if none exists.
      * @param values {@link ValuesDelta} associated with the view, or null if none exists.
      * @param viewIndex index of the view in the parent {@link Editor}, if it's a leave view.
      *     Otherwise, pass {@link #NO_VIEW_INDEX}.
      */
-    public int getId(EntityDelta entity, DataKind kind, ValuesDelta values,
+    public int getId(RawContactDelta entity, DataKind kind, ValuesDelta values,
             int viewIndex) {
         final String k = getMapKey(entity, kind, values, viewIndex);
 
@@ -81,7 +81,7 @@
         return id;
     }
 
-    private static String getMapKey(EntityDelta entity, DataKind kind, ValuesDelta values,
+    private static String getMapKey(RawContactDelta entity, DataKind kind, ValuesDelta values,
             int viewIndex) {
         sWorkStringBuilder.setLength(0);
         if (entity != null) {
diff --git a/src/com/android/contacts/group/GroupBrowseListAdapter.java b/src/com/android/contacts/group/GroupBrowseListAdapter.java
index ae58f73..acea625 100644
--- a/src/com/android/contacts/group/GroupBrowseListAdapter.java
+++ b/src/com/android/contacts/group/GroupBrowseListAdapter.java
@@ -29,8 +29,8 @@
 
 import com.android.contacts.GroupListLoader;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 import com.android.internal.util.Objects;
 
 /**
diff --git a/src/com/android/contacts/group/GroupDetailDisplayUtils.java b/src/com/android/contacts/group/GroupDetailDisplayUtils.java
index 4865196..d4e43a0 100644
--- a/src/com/android/contacts/group/GroupDetailDisplayUtils.java
+++ b/src/com/android/contacts/group/GroupDetailDisplayUtils.java
@@ -23,8 +23,8 @@
 import android.widget.TextView;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 
 public class GroupDetailDisplayUtils {
 
diff --git a/src/com/android/contacts/group/GroupDetailFragment.java b/src/com/android/contacts/group/GroupDetailFragment.java
index 5f07c64..6294b40 100644
--- a/src/com/android/contacts/group/GroupDetailFragment.java
+++ b/src/com/android/contacts/group/GroupDetailFragment.java
@@ -53,8 +53,8 @@
 import com.android.contacts.list.ContactTileAdapter;
 import com.android.contacts.list.ContactTileAdapter.DisplayType;
 import com.android.contacts.list.ContactTileView;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 
 /**
  * Displays the details of a group and shows a list of actions possible for the group.
diff --git a/src/com/android/contacts/group/GroupEditorFragment.java b/src/com/android/contacts/group/GroupEditorFragment.java
index 5f9beb3..762867c 100644
--- a/src/com/android/contacts/group/GroupEditorFragment.java
+++ b/src/com/android/contacts/group/GroupEditorFragment.java
@@ -66,9 +66,9 @@
 import com.android.contacts.activities.GroupEditorActivity;
 import com.android.contacts.editor.SelectAccountDialogFragment;
 import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.util.ViewUtil;
 import com.android.internal.util.Objects;
diff --git a/src/com/android/contacts/interactions/ContactDeletionInteraction.java b/src/com/android/contacts/interactions/ContactDeletionInteraction.java
index ead7010..ea6e6dc 100644
--- a/src/com/android/contacts/interactions/ContactDeletionInteraction.java
+++ b/src/com/android/contacts/interactions/ContactDeletionInteraction.java
@@ -35,8 +35,8 @@
 
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Sets;
 
diff --git a/src/com/android/contacts/interactions/GroupCreationDialogFragment.java b/src/com/android/contacts/interactions/GroupCreationDialogFragment.java
index 9eb4de9..5731bc1 100644
--- a/src/com/android/contacts/interactions/GroupCreationDialogFragment.java
+++ b/src/com/android/contacts/interactions/GroupCreationDialogFragment.java
@@ -23,7 +23,7 @@
 
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
 
 /**
  * A dialog for creating a new group.
diff --git a/src/com/android/contacts/interactions/ImportExportDialogFragment.java b/src/com/android/contacts/interactions/ImportExportDialogFragment.java
index 94c0616..f63f84c 100644
--- a/src/com/android/contacts/interactions/ImportExportDialogFragment.java
+++ b/src/com/android/contacts/interactions/ImportExportDialogFragment.java
@@ -40,7 +40,7 @@
 import com.android.contacts.R;
 import com.android.contacts.editor.SelectAccountDialogFragment;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountSelectionUtil;
 import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.vcard.ExportVCardActivity;
diff --git a/src/com/android/contacts/interactions/PhoneNumberInteraction.java b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
index 22cbd7d..207ceea 100644
--- a/src/com/android/contacts/interactions/PhoneNumberInteraction.java
+++ b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
@@ -53,10 +53,10 @@
 import com.android.contacts.R;
 import com.android.contacts.activities.DialtactsActivity;
 import com.android.contacts.activities.TransactionSafeActivity;
-import com.android.contacts.model.AccountType;
-import com.android.contacts.model.AccountType.StringInflater;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountType.StringInflater;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.DataKind;
 import com.google.common.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
diff --git a/src/com/android/contacts/list/AccountFilterActivity.java b/src/com/android/contacts/list/AccountFilterActivity.java
index b9e5b03..c5a21ba 100644
--- a/src/com/android/contacts/list/AccountFilterActivity.java
+++ b/src/com/android/contacts/list/AccountFilterActivity.java
@@ -36,9 +36,9 @@
 
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
diff --git a/src/com/android/contacts/list/ContactListFilterController.java b/src/com/android/contacts/list/ContactListFilterController.java
index aec670f..ddcb1ae 100644
--- a/src/com/android/contacts/list/ContactListFilterController.java
+++ b/src/com/android/contacts/list/ContactListFilterController.java
@@ -20,7 +20,7 @@
 import android.preference.PreferenceManager;
 
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/src/com/android/contacts/list/ContactListFilterView.java b/src/com/android/contacts/list/ContactListFilterView.java
index 7763846..d0ecfe4 100644
--- a/src/com/android/contacts/list/ContactListFilterView.java
+++ b/src/com/android/contacts/list/ContactListFilterView.java
@@ -26,8 +26,8 @@
 import android.widget.TextView;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.account.AccountType;
 
 /**
  * Contact list filter parameters.
diff --git a/src/com/android/contacts/list/CustomContactListFilterActivity.java b/src/com/android/contacts/list/CustomContactListFilterActivity.java
index a113f0c..8842a1d 100644
--- a/src/com/android/contacts/list/CustomContactListFilterActivity.java
+++ b/src/com/android/contacts/list/CustomContactListFilterActivity.java
@@ -27,7 +27,6 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.EntityIterator;
 import android.content.Intent;
 import android.content.Loader;
 import android.content.OperationApplicationException;
@@ -56,11 +55,11 @@
 
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.model.GoogleAccountType;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
+import com.android.contacts.model.account.GoogleAccountType;
 import com.android.contacts.util.EmptyService;
 import com.android.contacts.util.LocalizedNameResolver;
 import com.android.contacts.util.WeakAsyncTask;
@@ -146,7 +145,8 @@
                 if (account.dataSet != null) {
                     groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build();
                 }
-                EntityIterator iterator = ContactsContract.Groups.newEntityIterator(resolver.query(
+                android.content.EntityIterator iterator =
+                        ContactsContract.Groups.newEntityIterator(resolver.query(
                         groupsUri.build(), null, null, null, null));
                 try {
                     boolean hasGroups = false;
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index 7c56e15..64f3a91 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -45,6 +45,14 @@
 
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.list.ContactListFilterController;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountTypeWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
+import com.android.contacts.model.account.ExchangeAccountType;
+import com.android.contacts.model.account.ExternalAccountType;
+import com.android.contacts.model.account.FallbackAccountType;
+import com.android.contacts.model.account.GoogleAccountType;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.Constants;
 import com.android.internal.util.Objects;
 import com.google.common.annotations.VisibleForTesting;
diff --git a/src/com/android/contacts/model/Contact.java b/src/com/android/contacts/model/Contact.java
new file mode 100644
index 0000000..ede2101
--- /dev/null
+++ b/src/com/android/contacts/model/Contact.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright (C) 2012 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.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+
+import com.android.contacts.GroupMetaData;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.dataitem.DataItem;
+import com.android.contacts.util.DataStatus;
+import com.android.contacts.util.StreamItemEntry;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.ArrayList;
+
+/**
+ * A Contact represents a single person or logical entity as perceived by the user.  The information
+ * about a contact can come from multiple data sources, which are each represented by a RawContact
+ * object.  Thus, a Contact is associated with a collection of RawContact objects.
+ *
+ * The aggregation of raw contacts into a single contact is performed automatically, and it is
+ * also possible for users to manually split and join raw contacts into various contacts.
+ *
+ * Only the {@link ContactLoader} class can create a Contact object with various flags to allow
+ * partial loading of contact data.  Thus, an instance of this class should be treated as
+ * a read-only object.
+ */
+public class Contact {
+    private enum Status {
+        /** Contact is successfully loaded */
+        LOADED,
+        /** There was an error loading the contact */
+        ERROR,
+        /** Contact is not found */
+        NOT_FOUND,
+    }
+
+    private final Uri mRequestedUri;
+    private final Uri mLookupUri;
+    private final Uri mUri;
+    private final long mDirectoryId;
+    private final String mLookupKey;
+    private final long mId;
+    private final long mNameRawContactId;
+    private final int mDisplayNameSource;
+    private final long mPhotoId;
+    private final String mPhotoUri;
+    private final String mDisplayName;
+    private final String mAltDisplayName;
+    private final String mPhoneticName;
+    private final boolean mStarred;
+    private final Integer mPresence;
+    private ImmutableList<RawContact> mRawContacts;
+    private ImmutableList<StreamItemEntry> mStreamItems;
+    private ImmutableMap<Long,DataStatus> mStatuses;
+    private ImmutableList<AccountType> mInvitableAccountTypes;
+
+    private String mDirectoryDisplayName;
+    private String mDirectoryType;
+    private String mDirectoryAccountType;
+    private String mDirectoryAccountName;
+    private int mDirectoryExportSupport;
+
+    private ImmutableList<GroupMetaData> mGroups;
+
+    private byte[] mPhotoBinaryData;
+    private final boolean mSendToVoicemail;
+    private final String mCustomRingtone;
+    private final boolean mIsUserProfile;
+
+    private final Contact.Status mStatus;
+    private final Exception mException;
+
+    /**
+     * Constructor for special results, namely "no contact found" and "error".
+     */
+    private Contact(Uri requestedUri, Contact.Status status, Exception exception) {
+        if (status == Status.ERROR && exception == null) {
+            throw new IllegalArgumentException("ERROR result must have exception");
+        }
+        mStatus = status;
+        mException = exception;
+        mRequestedUri = requestedUri;
+        mLookupUri = null;
+        mUri = null;
+        mDirectoryId = -1;
+        mLookupKey = null;
+        mId = -1;
+        mRawContacts = null;
+        mStreamItems = null;
+        mStatuses = null;
+        mNameRawContactId = -1;
+        mDisplayNameSource = DisplayNameSources.UNDEFINED;
+        mPhotoId = -1;
+        mPhotoUri = null;
+        mDisplayName = null;
+        mAltDisplayName = null;
+        mPhoneticName = null;
+        mStarred = false;
+        mPresence = null;
+        mInvitableAccountTypes = null;
+        mSendToVoicemail = false;
+        mCustomRingtone = null;
+        mIsUserProfile = false;
+    }
+
+    public static Contact forError(Uri requestedUri, Exception exception) {
+        return new Contact(requestedUri, Status.ERROR, exception);
+    }
+
+    public static Contact forNotFound(Uri requestedUri) {
+        return new Contact(requestedUri, Status.NOT_FOUND, null);
+    }
+
+    /**
+     * Constructor to call when contact was found
+     */
+    public Contact(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey,
+            long id, long nameRawContactId, int displayNameSource, long photoId,
+            String photoUri, String displayName, String altDisplayName, String phoneticName,
+            boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone,
+            boolean isUserProfile) {
+        mStatus = Status.LOADED;
+        mException = null;
+        mRequestedUri = requestedUri;
+        mLookupUri = lookupUri;
+        mUri = uri;
+        mDirectoryId = directoryId;
+        mLookupKey = lookupKey;
+        mId = id;
+        mRawContacts = null;
+        mStreamItems = null;
+        mStatuses = null;
+        mNameRawContactId = nameRawContactId;
+        mDisplayNameSource = displayNameSource;
+        mPhotoId = photoId;
+        mPhotoUri = photoUri;
+        mDisplayName = displayName;
+        mAltDisplayName = altDisplayName;
+        mPhoneticName = phoneticName;
+        mStarred = starred;
+        mPresence = presence;
+        mInvitableAccountTypes = null;
+        mSendToVoicemail = sendToVoicemail;
+        mCustomRingtone = customRingtone;
+        mIsUserProfile = isUserProfile;
+    }
+
+    public Contact(Uri requestedUri, Contact from) {
+        mRequestedUri = requestedUri;
+
+        mStatus = from.mStatus;
+        mException = from.mException;
+        mLookupUri = from.mLookupUri;
+        mUri = from.mUri;
+        mDirectoryId = from.mDirectoryId;
+        mLookupKey = from.mLookupKey;
+        mId = from.mId;
+        mNameRawContactId = from.mNameRawContactId;
+        mDisplayNameSource = from.mDisplayNameSource;
+        mPhotoId = from.mPhotoId;
+        mPhotoUri = from.mPhotoUri;
+        mDisplayName = from.mDisplayName;
+        mAltDisplayName = from.mAltDisplayName;
+        mPhoneticName = from.mPhoneticName;
+        mStarred = from.mStarred;
+        mPresence = from.mPresence;
+        mRawContacts = from.mRawContacts;
+        mStreamItems = from.mStreamItems;
+        mStatuses = from.mStatuses;
+        mInvitableAccountTypes = from.mInvitableAccountTypes;
+
+        mDirectoryDisplayName = from.mDirectoryDisplayName;
+        mDirectoryType = from.mDirectoryType;
+        mDirectoryAccountType = from.mDirectoryAccountType;
+        mDirectoryAccountName = from.mDirectoryAccountName;
+        mDirectoryExportSupport = from.mDirectoryExportSupport;
+
+        mGroups = from.mGroups;
+
+        mPhotoBinaryData = from.mPhotoBinaryData;
+        mSendToVoicemail = from.mSendToVoicemail;
+        mCustomRingtone = from.mCustomRingtone;
+        mIsUserProfile = from.mIsUserProfile;
+    }
+
+    /**
+     * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
+     */
+    public void setDirectoryMetaData(String displayName, String directoryType,
+            String accountType, String accountName, int exportSupport) {
+        mDirectoryDisplayName = displayName;
+        mDirectoryType = directoryType;
+        mDirectoryAccountType = accountType;
+        mDirectoryAccountName = accountName;
+        mDirectoryExportSupport = exportSupport;
+    }
+
+    /* package */ void setPhotoBinaryData(byte[] photoBinaryData) {
+        mPhotoBinaryData = photoBinaryData;
+    }
+
+    /**
+     * Returns the URI for the contact that contains both the lookup key and the ID. This is
+     * the best URI to reference a contact.
+     * For directory contacts, this is the same a the URI as returned by {@link #getUri()}
+     */
+    public Uri getLookupUri() {
+        return mLookupUri;
+    }
+
+    public String getLookupKey() {
+        return mLookupKey;
+    }
+
+    /**
+     * Returns the contact Uri that was passed to the provider to make the query. This is
+     * the same as the requested Uri, unless the requested Uri doesn't specify a Contact:
+     * If it either references a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will
+     * always reference the full aggregate contact.
+     */
+    public Uri getUri() {
+        return mUri;
+    }
+
+    /**
+     * Returns the URI for which this {@link ContactLoader) was initially requested.
+     */
+    public Uri getRequestedUri() {
+        return mRequestedUri;
+    }
+
+    /**
+     * Instantiate a new RawContactDeltaList for this contact.
+     */
+    public RawContactDeltaList createRawContactDeltaList() {
+        return RawContactDeltaList.fromIterator(getRawContacts().iterator());
+    }
+
+    /**
+     * Returns the contact ID.
+     */
+    @VisibleForTesting
+    /* package */ long getId() {
+        return mId;
+    }
+
+    /**
+     * @return true when an exception happened during loading, in which case
+     *     {@link #getException} returns the actual exception object.
+     *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
+     *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
+     *     and vice versa.
+     */
+    public boolean isError() {
+        return mStatus == Status.ERROR;
+    }
+
+    public Exception getException() {
+        return mException;
+    }
+
+    /**
+     * @return true when the specified contact is not found.
+     *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
+     *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
+     *     and vice versa.
+     */
+    public boolean isNotFound() {
+        return mStatus == Status.NOT_FOUND;
+    }
+
+    /**
+     * @return true if the specified contact is successfully loaded.
+     *     i.e. neither {@link #isError()} nor {@link #isNotFound()}.
+     */
+    public boolean isLoaded() {
+        return mStatus == Status.LOADED;
+    }
+
+    public long getNameRawContactId() {
+        return mNameRawContactId;
+    }
+
+    public int getDisplayNameSource() {
+        return mDisplayNameSource;
+    }
+
+    public long getPhotoId() {
+        return mPhotoId;
+    }
+
+    public String getPhotoUri() {
+        return mPhotoUri;
+    }
+
+    public String getDisplayName() {
+        return mDisplayName;
+    }
+
+    public String getAltDisplayName() {
+        return mAltDisplayName;
+    }
+
+    public String getPhoneticName() {
+        return mPhoneticName;
+    }
+
+    public boolean getStarred() {
+        return mStarred;
+    }
+
+    public Integer getPresence() {
+        return mPresence;
+    }
+
+    /**
+     * This can return non-null invitable account types only if the {@link ContactLoader} was
+     * configured to load invitable account types in its constructor.
+     * @return
+     */
+    public ImmutableList<AccountType> getInvitableAccountTypes() {
+        return mInvitableAccountTypes;
+    }
+
+    public ImmutableList<RawContact> getRawContacts() {
+        return mRawContacts;
+    }
+
+    /**
+     * This can return non-null stream items only if the {@link ContactLoader} was
+     * configured to load stream items in its constructor.
+     * @return
+     */
+    public ImmutableList<StreamItemEntry> getStreamItems() {
+        return mStreamItems;
+    }
+
+    public ImmutableMap<Long, DataStatus> getStatuses() {
+        return mStatuses;
+    }
+
+    public long getDirectoryId() {
+        return mDirectoryId;
+    }
+
+    public boolean isDirectoryEntry() {
+        return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
+                && mDirectoryId != Directory.LOCAL_INVISIBLE;
+    }
+
+    /**
+     * @return true if this is a contact (not group, etc.) with at least one
+     *         writable raw-contact, and false otherwise.
+     */
+    public boolean isWritableContact(final Context context) {
+        return getFirstWritableRawContactId(context) != -1;
+    }
+
+    /**
+     * Return the ID of the first raw-contact in the contact data that belongs to a
+     * contact-writable account, or -1 if no such entity exists.
+     */
+    public long getFirstWritableRawContactId(final Context context) {
+        // Directory entries are non-writable
+        if (isDirectoryEntry()) return -1;
+
+        // Iterate through raw-contacts; if we find a writable on, return its ID.
+        for (RawContact rawContact : getRawContacts()) {
+            AccountType accountType = rawContact.getAccountType();
+            if (accountType != null && accountType.areContactsWritable()) {
+                return rawContact.getId();
+            }
+        }
+        // No writable raw-contact was found.
+        return -1;
+    }
+
+    public int getDirectoryExportSupport() {
+        return mDirectoryExportSupport;
+    }
+
+    public String getDirectoryDisplayName() {
+        return mDirectoryDisplayName;
+    }
+
+    public String getDirectoryType() {
+        return mDirectoryType;
+    }
+
+    public String getDirectoryAccountType() {
+        return mDirectoryAccountType;
+    }
+
+    public String getDirectoryAccountName() {
+        return mDirectoryAccountName;
+    }
+
+    public byte[] getPhotoBinaryData() {
+        return mPhotoBinaryData;
+    }
+
+    public ArrayList<ContentValues> getContentValues() {
+        if (mRawContacts.size() != 1) {
+            throw new IllegalStateException(
+                    "Cannot extract content values from an aggregated contact");
+        }
+
+        RawContact rawContact = mRawContacts.get(0);
+        ArrayList<ContentValues> result = new ArrayList<ContentValues>();
+        for (DataItem dataItem : rawContact.getDataItems()) {
+            result.add(dataItem.getContentValues());
+        }
+
+        // If the photo was loaded using the URI, create an entry for the photo
+        // binary data.
+        if (mPhotoId == 0 && mPhotoBinaryData != null) {
+            ContentValues photo = new ContentValues();
+            photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+            photo.put(Photo.PHOTO, mPhotoBinaryData);
+            result.add(photo);
+        }
+
+        return result;
+    }
+
+    /**
+     * This can return non-null group meta-data only if the {@link ContactLoader} was configured to
+     * load group metadata in its constructor.
+     * @return
+     */
+    public ImmutableList<GroupMetaData> getGroupMetaData() {
+        return mGroups;
+    }
+
+    public boolean isSendToVoicemail() {
+        return mSendToVoicemail;
+    }
+
+    public String getCustomRingtone() {
+        return mCustomRingtone;
+    }
+
+    public boolean isUserProfile() {
+        return mIsUserProfile;
+    }
+
+    @Override
+    public String toString() {
+        return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey +
+                ",uri=" + mUri + ",status=" + mStatus + "}";
+    }
+
+    /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) {
+        mRawContacts = rawContacts;
+    }
+
+    /* package */ void setStatuses(ImmutableMap<Long, DataStatus> statuses) {
+        mStatuses = statuses;
+    }
+
+    /* package */ void setInvitableAccountTypes(ImmutableList<AccountType> accountTypes) {
+        mInvitableAccountTypes = accountTypes;
+    }
+
+    /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) {
+        mGroups = groups;
+    }
+
+    /* package */ void setStreamItems(ImmutableList<StreamItemEntry> streamItems) {
+        mStreamItems = streamItems;
+    }
+}
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/model/ContactLoader.java
similarity index 63%
rename from src/com/android/contacts/ContactLoader.java
rename to src/com/android/contacts/model/ContactLoader.java
index c0aacae..6db997e 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/model/ContactLoader.java
@@ -14,15 +14,13 @@
  * limitations under the License
  */
 
-package com.android.contacts;
+package com.android.contacts.model;
 
 import android.content.AsyncTaskLoader;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -32,11 +30,9 @@
 import android.net.Uri;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Directory;
-import android.provider.ContactsContract.DisplayNameSources;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.StreamItemPhotos;
@@ -45,17 +41,18 @@
 import android.util.Log;
 import android.util.LongSparseArray;
 
-import com.android.contacts.model.AccountType;
-import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountTypeWithDataSet;
-import com.android.contacts.model.EntityDeltaList;
+import com.android.contacts.GroupMetaData;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountTypeWithDataSet;
+import com.android.contacts.model.dataitem.DataItem;
+import com.android.contacts.model.dataitem.PhotoDataItem;
 import com.android.contacts.util.ContactLoaderUtils;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.StreamItemEntry;
 import com.android.contacts.util.StreamItemPhotoEntry;
 import com.android.contacts.util.UriUtils;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
@@ -64,20 +61,19 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 /**
  * Loads a single Contact and all it constituent RawContacts.
  */
-public class ContactLoader extends AsyncTaskLoader<ContactLoader.Result> {
+public class ContactLoader extends AsyncTaskLoader<Contact> {
     private static final String TAG = ContactLoader.class.getSimpleName();
 
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     /** A short-lived cache that can be set by {@link #cacheResult()} */
-    private static Result sCachedResult = null;
+    private static Contact sCachedResult = null;
 
     private final Uri mRequestedUri;
     private Uri mLookupUri;
@@ -85,7 +81,7 @@
     private boolean mLoadStreamItems;
     private boolean mLoadInvitableAccountTypes;
     private boolean mPostViewNotification;
-    private Result mContact;
+    private Contact mContact;
     private ForceLoadContentObserver mObserver;
     private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
 
@@ -106,433 +102,6 @@
     }
 
     /**
-     * The result of a load operation. Contains all data necessary to display the contact.
-     */
-    public static final class Result {
-        private enum Status {
-            /** Contact is successfully loaded */
-            LOADED,
-            /** There was an error loading the contact */
-            ERROR,
-            /** Contact is not found */
-            NOT_FOUND,
-        }
-
-        private final Uri mRequestedUri;
-        private final Uri mLookupUri;
-        private final Uri mUri;
-        private final long mDirectoryId;
-        private final String mLookupKey;
-        private final long mId;
-        private final long mNameRawContactId;
-        private final int mDisplayNameSource;
-        private final long mPhotoId;
-        private final String mPhotoUri;
-        private final String mDisplayName;
-        private final String mAltDisplayName;
-        private final String mPhoneticName;
-        private final boolean mStarred;
-        private final Integer mPresence;
-        private final ArrayList<Entity> mEntities;
-        private ArrayList<StreamItemEntry> mStreamItems;
-        private final LongSparseArray<DataStatus> mStatuses;
-        private ArrayList<AccountType> mInvitableAccountTypes;
-
-        private String mDirectoryDisplayName;
-        private String mDirectoryType;
-        private String mDirectoryAccountType;
-        private String mDirectoryAccountName;
-        private int mDirectoryExportSupport;
-
-        private ArrayList<GroupMetaData> mGroups;
-
-        private byte[] mPhotoBinaryData;
-        private final boolean mSendToVoicemail;
-        private final String mCustomRingtone;
-        private final boolean mIsUserProfile;
-
-        private final Status mStatus;
-        private final Exception mException;
-
-        /**
-         * Constructor for special results, namely "no contact found" and "error".
-         */
-        private Result(Uri requestedUri, Status status, Exception exception) {
-            if (status == Status.ERROR && exception == null) {
-                throw new IllegalArgumentException("ERROR result must have exception");
-            }
-            mStatus = status;
-            mException = exception;
-            mRequestedUri = requestedUri;
-            mLookupUri = null;
-            mUri = null;
-            mDirectoryId = -1;
-            mLookupKey = null;
-            mId = -1;
-            mEntities = null;
-            mStreamItems = null;
-            mStatuses = null;
-            mNameRawContactId = -1;
-            mDisplayNameSource = DisplayNameSources.UNDEFINED;
-            mPhotoId = -1;
-            mPhotoUri = null;
-            mDisplayName = null;
-            mAltDisplayName = null;
-            mPhoneticName = null;
-            mStarred = false;
-            mPresence = null;
-            mInvitableAccountTypes = null;
-            mSendToVoicemail = false;
-            mCustomRingtone = null;
-            mIsUserProfile = false;
-        }
-
-        private static Result forError(Uri requestedUri, Exception exception) {
-            return new Result(requestedUri, Status.ERROR, exception);
-        }
-
-        private static Result forNotFound(Uri requestedUri) {
-            return new Result(requestedUri, Status.NOT_FOUND, null);
-        }
-
-        /**
-         * Constructor to call when contact was found
-         */
-        private Result(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey,
-                long id, long nameRawContactId, int displayNameSource, long photoId,
-                String photoUri, String displayName, String altDisplayName, String phoneticName,
-                boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone,
-                boolean isUserProfile) {
-            mStatus = Status.LOADED;
-            mException = null;
-            mRequestedUri = requestedUri;
-            mLookupUri = lookupUri;
-            mUri = uri;
-            mDirectoryId = directoryId;
-            mLookupKey = lookupKey;
-            mId = id;
-            mEntities = new ArrayList<Entity>();
-            mStreamItems = null;
-            mStatuses = new LongSparseArray<DataStatus>();
-            mNameRawContactId = nameRawContactId;
-            mDisplayNameSource = displayNameSource;
-            mPhotoId = photoId;
-            mPhotoUri = photoUri;
-            mDisplayName = displayName;
-            mAltDisplayName = altDisplayName;
-            mPhoneticName = phoneticName;
-            mStarred = starred;
-            mPresence = presence;
-            mInvitableAccountTypes = null;
-            mSendToVoicemail = sendToVoicemail;
-            mCustomRingtone = customRingtone;
-            mIsUserProfile = isUserProfile;
-        }
-
-        private Result(Uri requestedUri, Result from) {
-            mRequestedUri = requestedUri;
-
-            mStatus = from.mStatus;
-            mException = from.mException;
-            mLookupUri = from.mLookupUri;
-            mUri = from.mUri;
-            mDirectoryId = from.mDirectoryId;
-            mLookupKey = from.mLookupKey;
-            mId = from.mId;
-            mNameRawContactId = from.mNameRawContactId;
-            mDisplayNameSource = from.mDisplayNameSource;
-            mPhotoId = from.mPhotoId;
-            mPhotoUri = from.mPhotoUri;
-            mDisplayName = from.mDisplayName;
-            mAltDisplayName = from.mAltDisplayName;
-            mPhoneticName = from.mPhoneticName;
-            mStarred = from.mStarred;
-            mPresence = from.mPresence;
-            mEntities = from.mEntities;
-            mStreamItems = from.mStreamItems;
-            mStatuses = from.mStatuses;
-            mInvitableAccountTypes = from.mInvitableAccountTypes;
-
-            mDirectoryDisplayName = from.mDirectoryDisplayName;
-            mDirectoryType = from.mDirectoryType;
-            mDirectoryAccountType = from.mDirectoryAccountType;
-            mDirectoryAccountName = from.mDirectoryAccountName;
-            mDirectoryExportSupport = from.mDirectoryExportSupport;
-
-            mGroups = from.mGroups;
-
-            mPhotoBinaryData = from.mPhotoBinaryData;
-            mSendToVoicemail = from.mSendToVoicemail;
-            mCustomRingtone = from.mCustomRingtone;
-            mIsUserProfile = from.mIsUserProfile;
-        }
-
-        /**
-         * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
-         */
-        private void setDirectoryMetaData(String displayName, String directoryType,
-                String accountType, String accountName, int exportSupport) {
-            mDirectoryDisplayName = displayName;
-            mDirectoryType = directoryType;
-            mDirectoryAccountType = accountType;
-            mDirectoryAccountName = accountName;
-            mDirectoryExportSupport = exportSupport;
-        }
-
-        private void setPhotoBinaryData(byte[] photoBinaryData) {
-            mPhotoBinaryData = photoBinaryData;
-        }
-
-        /**
-         * Returns the URI for the contact that contains both the lookup key and the ID. This is
-         * the best URI to reference a contact.
-         * For directory contacts, this is the same a the URI as returned by {@link #getUri()}
-         */
-        public Uri getLookupUri() {
-            return mLookupUri;
-        }
-
-        public String getLookupKey() {
-            return mLookupKey;
-        }
-
-        /**
-         * Returns the contact Uri that was passed to the provider to make the query. This is
-         * the same as the requested Uri, unless the requested Uri doesn't specify a Contact:
-         * If it either references a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will
-         * always reference the full aggregate contact.
-         */
-        public Uri getUri() {
-            return mUri;
-        }
-
-        /**
-         * Returns the URI for which this {@link ContactLoader) was initially requested.
-         */
-        public Uri getRequestedUri() {
-            return mRequestedUri;
-        }
-
-        /**
-         * Instantiate a new EntityDeltaList for this contact.
-         */
-        public EntityDeltaList createEntityDeltaList() {
-            return EntityDeltaList.fromIterator(getEntities().iterator());
-        }
-
-        /**
-         * Returns the contact ID.
-         */
-        @VisibleForTesting
-        /* package */ long getId() {
-            return mId;
-        }
-
-        /**
-         * @return true when an exception happened during loading, in which case
-         *     {@link #getException} returns the actual exception object.
-         *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
-         *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
-         *     and vice versa.
-         */
-        public boolean isError() {
-            return mStatus == Status.ERROR;
-        }
-
-        public Exception getException() {
-            return mException;
-        }
-
-        /**
-         * @return true when the specified contact is not found.
-         *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
-         *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
-         *     and vice versa.
-         */
-        public boolean isNotFound() {
-            return mStatus == Status.NOT_FOUND;
-        }
-
-        /**
-         * @return true if the specified contact is successfully loaded.
-         *     i.e. neither {@link #isError()} nor {@link #isNotFound()}.
-         */
-        public boolean isLoaded() {
-            return mStatus == Status.LOADED;
-        }
-
-        public long getNameRawContactId() {
-            return mNameRawContactId;
-        }
-
-        public int getDisplayNameSource() {
-            return mDisplayNameSource;
-        }
-
-        public long getPhotoId() {
-            return mPhotoId;
-        }
-
-        public String getPhotoUri() {
-            return mPhotoUri;
-        }
-
-        public String getDisplayName() {
-            return mDisplayName;
-        }
-
-        public String getAltDisplayName() {
-            return mAltDisplayName;
-        }
-
-        public String getPhoneticName() {
-            return mPhoneticName;
-        }
-
-        public boolean getStarred() {
-            return mStarred;
-        }
-
-        public Integer getPresence() {
-            return mPresence;
-        }
-
-        public ArrayList<AccountType> getInvitableAccountTypes() {
-            return mInvitableAccountTypes;
-        }
-
-        public ArrayList<Entity> getEntities() {
-            return mEntities;
-        }
-
-        public ArrayList<StreamItemEntry> getStreamItems() {
-            return mStreamItems;
-        }
-
-        public LongSparseArray<DataStatus> getStatuses() {
-            return mStatuses;
-        }
-
-        public long getDirectoryId() {
-            return mDirectoryId;
-        }
-
-        public boolean isDirectoryEntry() {
-            return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
-                    && mDirectoryId != Directory.LOCAL_INVISIBLE;
-        }
-
-        /**
-         * @return true if this is a contact (not group, etc.) with at least one
-         *         writable raw-contact, and false otherwise.
-         */
-        public boolean isWritableContact(final Context context) {
-            return getFirstWritableRawContactId(context) != -1;
-        }
-
-        /**
-         * Return the ID of the first raw-contact in the contact data that belongs to a
-         * contact-writable account, or -1 if no such entity exists.
-         */
-        public long getFirstWritableRawContactId(final Context context) {
-            // Directory entries are non-writable
-            if (isDirectoryEntry()) return -1;
-
-            // Iterate through raw-contacts; if we find a writable on, return its ID.
-            final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
-            for (Entity entity : getEntities()) {
-                ContentValues values = entity.getEntityValues();
-                String type = values.getAsString(RawContacts.ACCOUNT_TYPE);
-                String dataSet = values.getAsString(RawContacts.DATA_SET);
-
-                AccountType accountType = accountTypes.getAccountType(type, dataSet);
-                if (accountType != null && accountType.areContactsWritable()) {
-                    return values.getAsLong(RawContacts._ID);
-                }
-            }
-            // No writable raw-contact was found.
-            return -1;
-        }
-
-        public int getDirectoryExportSupport() {
-            return mDirectoryExportSupport;
-        }
-
-        public String getDirectoryDisplayName() {
-            return mDirectoryDisplayName;
-        }
-
-        public String getDirectoryType() {
-            return mDirectoryType;
-        }
-
-        public String getDirectoryAccountType() {
-            return mDirectoryAccountType;
-        }
-
-        public String getDirectoryAccountName() {
-            return mDirectoryAccountName;
-        }
-
-        public byte[] getPhotoBinaryData() {
-            return mPhotoBinaryData;
-        }
-
-        public ArrayList<ContentValues> getContentValues() {
-            if (mEntities.size() != 1) {
-                throw new IllegalStateException(
-                        "Cannot extract content values from an aggregated contact");
-            }
-
-            Entity entity = mEntities.get(0);
-            ArrayList<ContentValues> result = new ArrayList<ContentValues>();
-            ArrayList<NamedContentValues> subValues = entity.getSubValues();
-            if (subValues != null) {
-                int size = subValues.size();
-                for (int i = 0; i < size; i++) {
-                    NamedContentValues pair = subValues.get(i);
-                    if (Data.CONTENT_URI.equals(pair.uri)) {
-                        result.add(pair.values);
-                    }
-                }
-            }
-
-            // If the photo was loaded using the URI, create an entry for the photo
-            // binary data.
-            if (mPhotoId == 0 && mPhotoBinaryData != null) {
-                ContentValues photo = new ContentValues();
-                photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
-                photo.put(Photo.PHOTO, mPhotoBinaryData);
-                result.add(photo);
-            }
-
-            return result;
-        }
-
-        public List<GroupMetaData> getGroupMetaData() {
-            return mGroups;
-        }
-
-        public boolean isSendToVoicemail() {
-            return mSendToVoicemail;
-        }
-
-        public String getCustomRingtone() {
-            return mCustomRingtone;
-        }
-
-        public boolean isUserProfile() {
-            return mIsUserProfile;
-        }
-
-        @Override
-        public String toString() {
-            return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey +
-                    ",uri=" + mUri + ",status=" + mStatus + "}";
-        }
-    }
-
-    /**
      * Projection used for the query that loads all data for the entire contact (except for
      * social stream items).
      */
@@ -726,21 +295,21 @@
     }
 
     @Override
-    public Result loadInBackground() {
+    public Contact loadInBackground() {
         try {
             final ContentResolver resolver = getContext().getContentResolver();
             final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(
                     resolver, mLookupUri);
-            final Result cachedResult = sCachedResult;
+            final Contact cachedResult = sCachedResult;
             sCachedResult = null;
             // Is this the same Uri as what we had before already? In that case, reuse that result
-            final Result result;
+            final Contact result;
             final boolean resultIsCached;
             if (cachedResult != null &&
                     UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
                 // We are using a cached result from earlier. Below, we should make sure
                 // we are not doing any more network or disc accesses
-                result = new Result(mRequestedUri, cachedResult);
+                result = new Contact(mRequestedUri, cachedResult);
                 resultIsCached = true;
             } else {
                 result = loadContactEntity(resolver, uriCurrentFormat);
@@ -769,57 +338,62 @@
             return result;
         } catch (Exception e) {
             Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
-            return Result.forError(mRequestedUri, e);
+            return Contact.forError(mRequestedUri, e);
         }
     }
 
-    private Result loadContactEntity(ContentResolver resolver, Uri contactUri) {
+    private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) {
         Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
         Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
                 Contacts.Entity.RAW_CONTACT_ID);
         if (cursor == null) {
             Log.e(TAG, "No cursor returned in loadContactEntity");
-            return Result.forNotFound(mRequestedUri);
+            return Contact.forNotFound(mRequestedUri);
         }
 
         try {
             if (!cursor.moveToFirst()) {
                 cursor.close();
-                return Result.forNotFound(mRequestedUri);
+                return Contact.forNotFound(mRequestedUri);
             }
 
-            // Create the loaded result starting with the Contact data.
-            Result result = loadContactHeaderData(cursor, contactUri);
+            // Create the loaded contact starting with the header data.
+            Contact contact = loadContactHeaderData(cursor, contactUri);
 
             // Fill in the raw contacts, which is wrapped in an Entity and any
             // status data.  Initially, result has empty entities and statuses.
             long currentRawContactId = -1;
-            Entity entity = null;
-            ArrayList<Entity> entities = result.getEntities();
-            LongSparseArray<DataStatus> statuses = result.getStatuses();
-            for (; !cursor.isAfterLast(); cursor.moveToNext()) {
+            RawContact rawContact = null;
+            ImmutableList.Builder<RawContact> rawContactsBuilder =
+                    new ImmutableList.Builder<RawContact>();
+            ImmutableMap.Builder<Long, DataStatus> statusesBuilder =
+                    new ImmutableMap.Builder<Long, DataStatus>();
+            do {
                 long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
                 if (rawContactId != currentRawContactId) {
                     // First time to see this raw contact id, so create a new entity, and
                     // add it to the result's entities.
                     currentRawContactId = rawContactId;
-                    entity = new android.content.Entity(loadRawContact(cursor));
-                    entities.add(entity);
+                    rawContact = new RawContact(getContext(), loadRawContactValues(cursor));
+                    rawContactsBuilder.add(rawContact);
                 }
                 if (!cursor.isNull(ContactQuery.DATA_ID)) {
-                    ContentValues data = loadData(cursor);
-                    entity.addSubValue(ContactsContract.Data.CONTENT_URI, data);
+                    ContentValues data = loadDataValues(cursor);
+                    rawContact.addDataItemValues(data);
 
                     if (!cursor.isNull(ContactQuery.PRESENCE)
                             || !cursor.isNull(ContactQuery.STATUS)) {
                         final DataStatus status = new DataStatus(cursor);
                         final long dataId = cursor.getLong(ContactQuery.DATA_ID);
-                        statuses.put(dataId, status);
+                        statusesBuilder.put(dataId, status);
                     }
                 }
-            }
+            } while (cursor.moveToNext());
 
-            return result;
+            contact.setRawContacts(rawContactsBuilder.build());
+            contact.setStatuses(statusesBuilder.build());
+
+            return contact;
         } finally {
             cursor.close();
         }
@@ -829,7 +403,7 @@
      * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
      * not found, returns null
      */
-    private void loadPhotoBinaryData(Result contactData) {
+    private void loadPhotoBinaryData(Contact contactData) {
 
         // If we have a photo URI, try loading that first.
         String photoUri = contactData.getPhotoUri();
@@ -863,17 +437,15 @@
             return;
         }
 
-        for (Entity entity : contactData.getEntities()) {
-            for (NamedContentValues subValue : entity.getSubValues()) {
-                final ContentValues entryValues = subValue.values;
-                final long dataId = entryValues.getAsLong(Data._ID);
-                if (dataId == photoId) {
-                    final String mimeType = entryValues.getAsString(Data.MIMETYPE);
-                    // Correct Data Id but incorrect MimeType? Don't load
-                    if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                        return;
+        for (RawContact rawContact : contactData.getRawContacts()) {
+            for (DataItem dataItem : rawContact.getDataItems()) {
+                if (dataItem.getId() == photoId) {
+                    if (!(dataItem instanceof PhotoDataItem)) {
+                        break;
                     }
-                    contactData.setPhotoBinaryData(entryValues.getAsByteArray(Photo.PHOTO));
+
+                    final PhotoDataItem photo = (PhotoDataItem) dataItem;
+                    contactData.setPhotoBinaryData(photo.getPhoto());
                     break;
                 }
             }
@@ -881,10 +453,11 @@
     }
 
     /**
-     * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}.
+     * Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}.
      */
-    private void loadInvitableAccountTypes(Result contactData) {
-        final ArrayList<AccountType> resultList = Lists.newArrayList();
+    private void loadInvitableAccountTypes(Contact contactData) {
+        final ImmutableList.Builder<AccountType> resultListBuilder =
+                new ImmutableList.Builder<AccountType>();
         if (!contactData.isUserProfile()) {
             Map<AccountTypeWithDataSet, AccountType> invitables =
                     AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
@@ -893,26 +466,25 @@
                         Maps.newHashMap(invitables);
 
                 // Remove the ones that already have a raw contact in the current contact
-                for (Entity entity : contactData.getEntities()) {
-                    final ContentValues values = entity.getEntityValues();
+                for (RawContact rawContact : contactData.getRawContacts()) {
                     final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
-                            values.getAsString(RawContacts.ACCOUNT_TYPE),
-                            values.getAsString(RawContacts.DATA_SET));
+                            rawContact.getAccountTypeString(),
+                            rawContact.getDataSet());
                     resultMap.remove(type);
                 }
 
-                resultList.addAll(resultMap.values());
+                resultListBuilder.addAll(resultMap.values());
             }
         }
 
         // Set to mInvitableAccountTypes
-        contactData.mInvitableAccountTypes = resultList;
+        contactData.setInvitableAccountTypes(resultListBuilder.build());
     }
 
     /**
      * Extracts Contact level columns from the cursor.
      */
-    private Result loadContactHeaderData(final Cursor cursor, Uri contactUri) {
+    private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
         final String directoryParameter =
                 contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
         final long directoryId = directoryParameter == null
@@ -943,7 +515,7 @@
             lookupUri = contactUri;
         }
 
-        return new Result(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
+        return new Contact(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
                 contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
                 altDisplayName, phoneticName, starred, presence, sendToVoicemail,
                 customRingtone, isUserProfile);
@@ -952,7 +524,7 @@
     /**
      * Extracts RawContact level columns from the cursor.
      */
-    private ContentValues loadRawContact(Cursor cursor) {
+    private ContentValues loadRawContactValues(Cursor cursor) {
         ContentValues cv = new ContentValues();
 
         cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
@@ -979,7 +551,7 @@
     /**
      * Extracts Data level columns from the cursor.
      */
-    private ContentValues loadData(Cursor cursor) {
+    private ContentValues loadDataValues(Cursor cursor) {
         ContentValues cv = new ContentValues();
 
         cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
@@ -1034,7 +606,7 @@
         }
     }
 
-    private void loadDirectoryMetaData(Result result) {
+    private void loadDirectoryMetaData(Contact result) {
         long directoryId = result.getDirectoryId();
 
         Cursor cursor = getContext().getContentResolver().query(
@@ -1075,14 +647,13 @@
      * Loads groups meta-data for all groups associated with all constituent raw contacts'
      * accounts.
      */
-    private void loadGroupMetaData(Result result) {
+    private void loadGroupMetaData(Contact result) {
         StringBuilder selection = new StringBuilder();
         ArrayList<String> selectionArgs = new ArrayList<String>();
-        for (Entity entity : result.mEntities) {
-            ContentValues values = entity.getEntityValues();
-            String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
-            String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
-            String dataSet = values.getAsString(RawContacts.DATA_SET);
+        for (RawContact rawContact : result.getRawContacts()) {
+            final String accountName = rawContact.getAccountName();
+            final String accountType = rawContact.getAccountTypeString();
+            final String dataSet = rawContact.getDataSet();
             if (accountName != null && accountType != null) {
                 if (selection.length() != 0) {
                     selection.append(" OR ");
@@ -1101,7 +672,8 @@
                 selection.append(")");
             }
         }
-        final ArrayList<GroupMetaData> groupList = new ArrayList<GroupMetaData>();
+        final ImmutableList.Builder<GroupMetaData> groupListBuilder =
+                new ImmutableList.Builder<GroupMetaData>();
         final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
                 GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
                 null);
@@ -1119,28 +691,28 @@
                         ? false
                         : cursor.getInt(GroupQuery.FAVORITES) != 0;
 
-                groupList.add(new GroupMetaData(
+                groupListBuilder.add(new GroupMetaData(
                         accountName, accountType, dataSet, groupId, title, defaultGroup,
                         favorites));
             }
         } finally {
             cursor.close();
         }
-        result.mGroups = groupList;
+        result.setGroupMetaData(groupListBuilder.build());
     }
 
     /**
      * Loads all stream items and stream item photos belonging to this contact.
      */
-    private void loadStreamItems(Result result) {
-        Cursor cursor = getContext().getContentResolver().query(
+    private void loadStreamItems(Contact result) {
+        final Cursor cursor = getContext().getContentResolver().query(
                 Contacts.CONTENT_LOOKUP_URI.buildUpon()
                         .appendPath(result.getLookupKey())
                         .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(),
                 null, null, null, null);
-        LongSparseArray<StreamItemEntry> streamItemsById =
+        final LongSparseArray<StreamItemEntry> streamItemsById =
                 new LongSparseArray<StreamItemEntry>();
-        ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>();
+        final ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>();
         try {
             while (cursor.moveToNext()) {
                 StreamItemEntry streamItem = new StreamItemEntry(cursor);
@@ -1213,11 +785,13 @@
 
         // Set the sorted stream items on the result.
         Collections.sort(streamItems);
-        result.mStreamItems = streamItems;
+        result.setStreamItems(new ImmutableList.Builder<StreamItemEntry>()
+                .addAll(streamItems.iterator())
+                .build());
     }
 
     @Override
-    public void deliverResult(Result result) {
+    public void deliverResult(Contact result) {
         unregisterObserver();
 
         // The creator isn't interested in any further updates
@@ -1254,17 +828,13 @@
      */
     private void postViewNotificationToSyncAdapter() {
         Context context = getContext();
-        for (Entity entity : mContact.getEntities()) {
-            final ContentValues entityValues = entity.getEntityValues();
-            final long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID);
+        for (RawContact rawContact : mContact.getRawContacts()) {
+            final long rawContactId = rawContact.getId();
             if (mNotifiedRawContactIds.contains(rawContactId)) {
                 continue; // Already notified for this raw contact.
             }
             mNotifiedRawContactIds.add(rawContactId);
-            final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
-            final AccountType accountType = AccountTypeManager.getInstance(context).getAccountType(
-                    type, dataSet);
+            final AccountType accountType = rawContact.getAccountType();
             final String serviceName = accountType.getViewContactNotifyServiceClassName();
             final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
             if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
diff --git a/src/com/android/contacts/model/RawContact.java b/src/com/android/contacts/model/RawContact.java
new file mode 100644
index 0000000..3a193b4
--- /dev/null
+++ b/src/com/android/contacts/model/RawContact.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2012 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.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
+import com.android.contacts.model.dataitem.DataItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * RawContact represents a single raw contact in the raw contacts database.
+ * It has specialized getters/setters for raw contact
+ * items, and also contains a collection of DataItem objects.  A RawContact contains the information
+ * from a single account.
+ *
+ * This allows RawContact objects to be thought of as a class with raw contact
+ * fields (like account type, name, data set, sync state, etc.) and a list of
+ * DataItem objects that represent contact information elements (like phone
+ * numbers, email, address, etc.).
+ */
+public class RawContact {
+
+    private final Context mContext;
+    private AccountTypeManager mAccountTypeManager;
+    private final ContentValues mValues;
+    private final ArrayList<NamedDataItem> mDataItems;
+
+    public static class NamedDataItem {
+        public final Uri uri;
+        public final DataItem dataItem;
+
+        public NamedDataItem(Uri uri, DataItem dataItem) {
+            this.uri = uri;
+            this.dataItem = dataItem;
+        }
+    }
+
+    public static RawContact createFrom(Entity entity) {
+        final ContentValues values = entity.getEntityValues();
+        final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
+
+        RawContact rawContact = new RawContact(null, values);
+        for (Entity.NamedContentValues subValue : subValues) {
+            rawContact.addNamedDataItemValues(subValue.uri, subValue.values);
+        }
+        return rawContact;
+    }
+
+    /**
+     * A RawContact object can be created with or without a context.
+     *
+     * The context is used for the buildString() member function in DataItem objects,
+     * specifically for retrieving an instance of AccountTypeManager.  It is okay to
+     * pass in null for the context in which case, you will not be able to call buildString(),
+     * getDataKind(), or getAccountType() from a DataItem object.
+     */
+    public RawContact(Context context) {
+        this(context, new ContentValues());
+    }
+
+    public RawContact(Context context, ContentValues values) {
+        mContext = context;
+        mValues = values;
+        mDataItems = new ArrayList<NamedDataItem>();
+    }
+
+    public AccountTypeManager getAccountTypeManager() {
+        if (mAccountTypeManager == null) {
+            mAccountTypeManager = AccountTypeManager.getInstance(mContext);
+        }
+        return mAccountTypeManager;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public ContentValues getValues() {
+        return mValues;
+    }
+
+    /**
+     * Returns the id of the raw contact.
+     */
+    public Long getId() {
+        return getValues().getAsLong(RawContacts._ID);
+    }
+
+    /**
+     * Returns the account name of the raw contact.
+     */
+    public String getAccountName() {
+        return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+    }
+
+    /**
+     * Returns the account type of the raw contact.
+     */
+    public String getAccountTypeString() {
+        return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+    }
+
+    /**
+     * Returns the data set of the raw contact.
+     */
+    public String getDataSet() {
+        return getValues().getAsString(RawContacts.DATA_SET);
+    }
+
+    /**
+     * Returns the account type and data set of the raw contact.
+     */
+    public String getAccountTypeAndDataSetString() {
+        return getValues().getAsString(RawContacts.ACCOUNT_TYPE_AND_DATA_SET);
+    }
+
+    public boolean isDirty() {
+        return getValues().getAsBoolean(RawContacts.DIRTY);
+    }
+
+    public long getVersion() {
+        return getValues().getAsLong(RawContacts.DIRTY);
+    }
+
+    public String getSourceId() {
+        return getValues().getAsString(RawContacts.SOURCE_ID);
+    }
+
+    public String getSync1() {
+        return getValues().getAsString(RawContacts.SYNC1);
+    }
+
+    public String getSync2() {
+        return getValues().getAsString(RawContacts.SYNC2);
+    }
+
+    public String getSync3() {
+        return getValues().getAsString(RawContacts.SYNC3);
+    }
+
+    public String getSync4() {
+        return getValues().getAsString(RawContacts.SYNC4);
+    }
+
+    public boolean isDeleted() {
+        return getValues().getAsBoolean(RawContacts.DELETED);
+    }
+
+    public boolean isNameVerified() {
+        return getValues().getAsBoolean(RawContacts.NAME_VERIFIED);
+    }
+
+    public long getContactId() {
+        return getValues().getAsLong(Contacts.Entity.CONTACT_ID);
+    }
+
+    public boolean isStarred() {
+        return getValues().getAsBoolean(Contacts.STARRED);
+    }
+
+    public AccountType getAccountType() {
+        return getAccountTypeManager().getAccountType(getAccountTypeString(), getDataSet());
+    }
+
+    /**
+     * Sets the account name, account type, and data set strings.
+     * Valid combinations for account-name, account-type, data-set
+     * 1) null, null, null (local account)
+     * 2) non-null, non-null, null (valid account without data-set)
+     * 3) non-null, non-null, non-null (valid account with data-set)
+     */
+    private void setAccount(String accountName, String accountType, String dataSet) {
+        final ContentValues values = getValues();
+        if (accountName == null) {
+            if (accountType == null && dataSet == null) {
+                // This is a local account
+                values.putNull(RawContacts.ACCOUNT_NAME);
+                values.putNull(RawContacts.ACCOUNT_TYPE);
+                values.putNull(RawContacts.DATA_SET);
+                return;
+            }
+        } else {
+            if (accountType != null) {
+                // This is a valid account, either with or without a dataSet.
+                values.put(RawContacts.ACCOUNT_NAME, accountName);
+                values.put(RawContacts.ACCOUNT_TYPE, accountType);
+                if (dataSet == null) {
+                    values.putNull(RawContacts.DATA_SET);
+                } else {
+                    values.put(RawContacts.DATA_SET, dataSet);
+                }
+            }
+        }
+        throw new IllegalArgumentException(
+                "Not a valid combination of account name, type, and data set.");
+    }
+
+    public void setAccount(AccountWithDataSet accountWithDataSet) {
+        setAccount(accountWithDataSet.name, accountWithDataSet.type, accountWithDataSet.dataSet);
+    }
+
+    public void setAccountToLocal() {
+        setAccount(null, null, null);
+    }
+
+    public void addDataItemValues(ContentValues values) {
+        addNamedDataItemValues(Data.CONTENT_URI, values);
+    }
+
+    public void addNamedDataItemValues(Uri uri, ContentValues values) {
+        mDataItems.add(new NamedDataItem(uri, DataItem.createFrom(this, values)));
+    }
+
+    public List<DataItem> getDataItems() {
+        final ArrayList<DataItem> list = new ArrayList<DataItem>();
+        for (NamedDataItem dataItem : mDataItems) {
+            if (Data.CONTENT_URI.equals(dataItem.uri)) {
+                list.add(dataItem.dataItem);
+            }
+        }
+        return list;
+    }
+
+    public List<NamedDataItem> getNamedDataItems() {
+        return mDataItems;
+    }
+
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("RawContact: ").append(mValues);
+        for (RawContact.NamedDataItem namedDataItem : mDataItems) {
+            sb.append("\n  ").append(namedDataItem.uri);
+            sb.append("\n  -> ").append(namedDataItem.dataItem.getContentValues());
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/RawContactDelta.java
similarity index 83%
rename from src/com/android/contacts/model/EntityDelta.java
rename to src/com/android/contacts/model/RawContactDelta.java
index 31b5306..2ee9d5c 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/RawContactDelta.java
@@ -20,18 +20,22 @@
 import android.content.ContentProviderOperation.Builder;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
 import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.provider.BaseColumns;
+import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Profile;
 import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
 
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.dataitem.DataItem;
 import com.android.contacts.test.NeededForTesting;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -42,21 +46,20 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-
 /**
- * Contains an {@link Entity} and records any modifications separately so the
- * original {@link Entity} can be swapped out with a newer version and the
+ * Contains a {@link RawContact} and records any modifications separately so the
+ * original {@link RawContact} can be swapped out with a newer version and the
  * changes still cleanly applied.
  * <p>
  * One benefit of this approach is that we can build changes entirely on an
- * empty {@link Entity}, which then becomes an insert {@link RawContacts} case.
+ * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case.
  * <p>
- * When applying modifications over an {@link Entity}, we try finding the
+ * When applying modifications over an {@link RawContact}, we try finding the
  * original {@link Data#_ID} rows where the modifications took place. If those
- * rows are missing from the new {@link Entity}, we know the original data must
+ * rows are missing from the new {@link RawContact}, we know the original data must
  * be deleted, but to preserve the user modifications we treat as an insert.
  */
-public class EntityDelta implements Parcelable {
+public class RawContactDelta implements Parcelable {
     // TODO: optimize by using contentvalues pool, since we allocate so many of them
 
     private static final String TAG = "EntityDelta";
@@ -79,40 +82,40 @@
      */
     private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
 
-    public EntityDelta() {
+    public RawContactDelta() {
     }
 
-    public EntityDelta(ValuesDelta values) {
+    public RawContactDelta(ValuesDelta values) {
         mValues = values;
     }
 
     /**
-     * Build an {@link EntityDelta} using the given {@link Entity} as a
+     * Build an {@link RawContactDelta} using the given {@link RawContact} as a
      * starting point; the "before" snapshot.
      */
-    public static EntityDelta fromBefore(Entity before) {
-        final EntityDelta entity = new EntityDelta();
-        entity.mValues = ValuesDelta.fromBefore(before.getEntityValues());
-        entity.mValues.setIdColumn(RawContacts._ID);
-        for (NamedContentValues namedValues : before.getSubValues()) {
-            entity.addEntry(ValuesDelta.fromBefore(namedValues.values));
+    public static RawContactDelta fromBefore(RawContact before) {
+        final RawContactDelta rawContactDelta = new RawContactDelta();
+        rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
+        rawContactDelta.mValues.setIdColumn(RawContacts._ID);
+        for (DataItem dataItem : before.getDataItems()) {
+            rawContactDelta.addEntry(ValuesDelta.fromBefore(dataItem.getContentValues()));
         }
-        return entity;
+        return rawContactDelta;
     }
 
     /**
-     * Merge the "after" values from the given {@link EntityDelta} onto the
-     * "before" state represented by this {@link EntityDelta}, discarding any
+     * Merge the "after" values from the given {@link RawContactDelta} onto the
+     * "before" state represented by this {@link RawContactDelta}, discarding any
      * existing "after" states. This is typically used when re-parenting changes
      * onto an updated {@link Entity}.
      */
-    public static EntityDelta mergeAfter(EntityDelta local, EntityDelta remote) {
+    public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
         // Bail early if trying to merge delete with missing local
         final ValuesDelta remoteValues = remote.mValues;
         if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;
 
         // Create local version if none exists yet
-        if (local == null) local = new EntityDelta();
+        if (local == null) local = new RawContactDelta();
 
         if (LOGV) {
             final Long localVersion = (local.mValues == null) ? null : local.mValues
@@ -223,6 +226,26 @@
         return getValues().getAsLong(RawContacts._ID);
     }
 
+    public String getAccountName() {
+        return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+    }
+
+    public String getAccountType() {
+        return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+    }
+
+    public String getDataSet() {
+        return getValues().getAsString(RawContacts.DATA_SET);
+    }
+
+    public AccountType getAccountType(AccountTypeManager manager) {
+        return manager.getAccountType(getAccountType(), getDataSet());
+    }
+
+    public boolean isVisible() {
+        return getValues().isVisible();
+    }
+
     /**
      * Return the list of child {@link ValuesDelta} from our optimized map,
      * creating the list if requested.
@@ -308,8 +331,8 @@
 
     @Override
     public boolean equals(Object object) {
-        if (object instanceof EntityDelta) {
-            final EntityDelta other = (EntityDelta)object;
+        if (object instanceof RawContactDelta) {
+            final RawContactDelta other = (RawContactDelta)object;
 
             // Equality failed if parent values different
             if (!other.mValues.equals(mValues)) return false;
@@ -403,7 +426,7 @@
     /**
      * Build a list of {@link ContentProviderOperation} that will transform the
      * current "before" {@link Entity} state into the modified state which this
-     * {@link EntityDelta} represents.
+     * {@link RawContactDelta} represents.
      */
     public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
         final int firstIndex = buildInto.size();
@@ -524,15 +547,16 @@
         mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI;
     }
 
-    public static final Parcelable.Creator<EntityDelta> CREATOR = new Parcelable.Creator<EntityDelta>() {
-        public EntityDelta createFromParcel(Parcel in) {
-            final EntityDelta state = new EntityDelta();
+    public static final Parcelable.Creator<RawContactDelta> CREATOR =
+            new Parcelable.Creator<RawContactDelta>() {
+        public RawContactDelta createFromParcel(Parcel in) {
+            final RawContactDelta state = new RawContactDelta();
             state.readFromParcel(in);
             return state;
         }
 
-        public EntityDelta[] newArray(int size) {
-            return new EntityDelta[size];
+        public RawContactDelta[] newArray(int size) {
+            return new RawContactDelta[size];
         }
     };
 
@@ -962,5 +986,118 @@
                 return new ValuesDelta[size];
             }
         };
+
+        public void setGroupRowId(long groupId) {
+            put(GroupMembership.GROUP_ROW_ID, groupId);
+        }
+
+        public Long getGroupRowId() {
+            return getAsLong(GroupMembership.GROUP_ROW_ID);
+        }
+
+        public void setPhoto(byte[] value) {
+            put(Photo.PHOTO, value);
+        }
+
+        public byte[] getPhoto() {
+            return getAsByteArray(Photo.PHOTO);
+        }
+
+        public void setSuperPrimary(boolean val) {
+            if (val) {
+                put(Data.IS_SUPER_PRIMARY, 1);
+            } else {
+                put(Data.IS_SUPER_PRIMARY, 0);
+            }
+        }
+
+        public void setPhoneticFamilyName(String value) {
+            put(StructuredName.PHONETIC_FAMILY_NAME, value);
+        }
+
+        public void setPhoneticMiddleName(String value) {
+            put(StructuredName.PHONETIC_MIDDLE_NAME, value);
+        }
+
+        public void setPhoneticGivenName(String value) {
+            put(StructuredName.PHONETIC_GIVEN_NAME, value);
+        }
+
+        public String getPhoneticFamilyName() {
+            return getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+        }
+
+        public String getPhoneticMiddleName() {
+            return getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+        }
+
+        public String getPhoneticGivenName() {
+            return getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+        }
+
+        public String getDisplayName() {
+            return getAsString(StructuredName.DISPLAY_NAME);
+        }
+
+        public void setDisplayName(String name) {
+            if (name == null) {
+                putNull(StructuredName.DISPLAY_NAME);
+            } else {
+                put(StructuredName.DISPLAY_NAME, name);
+            }
+        }
+
+        public void copyStructuredNameFieldsFrom(ValuesDelta name) {
+            copyStringFrom(name, StructuredName.DISPLAY_NAME);
+
+            copyStringFrom(name, StructuredName.GIVEN_NAME);
+            copyStringFrom(name, StructuredName.FAMILY_NAME);
+            copyStringFrom(name, StructuredName.PREFIX);
+            copyStringFrom(name, StructuredName.MIDDLE_NAME);
+            copyStringFrom(name, StructuredName.SUFFIX);
+
+            copyStringFrom(name, StructuredName.PHONETIC_GIVEN_NAME);
+            copyStringFrom(name, StructuredName.PHONETIC_MIDDLE_NAME);
+            copyStringFrom(name, StructuredName.PHONETIC_FAMILY_NAME);
+
+            copyStringFrom(name, StructuredName.FULL_NAME_STYLE);
+            copyStringFrom(name, StructuredName.PHONETIC_NAME_STYLE);
+        }
+
+        public String getPhoneNumber() {
+            return getAsString(Phone.NUMBER);
+        }
+
+        public String getPhoneNormalizedNumber() {
+            return getAsString(Phone.NORMALIZED_NUMBER);
+        }
+
+        public boolean phoneHasType() {
+            return containsKey(Phone.TYPE);
+        }
+
+        public int getPhoneType() {
+            return getAsInteger(Phone.TYPE);
+        }
+
+        public String getPhoneLabel() {
+            return getAsString(Phone.LABEL);
+        }
+
+        public String getEmailData() {
+            return getAsString(Email.DATA);
+        }
+
+        public boolean emailHasType() {
+            return containsKey(Email.TYPE);
+        }
+
+        public int getEmailType() {
+            return getAsInteger(Email.TYPE);
+        }
+
+        public String getEmailLabel() {
+            return getAsString(Email.LABEL);
+        }
     }
 }
diff --git a/src/com/android/contacts/model/EntityDeltaList.java b/src/com/android/contacts/model/RawContactDeltaList.java
similarity index 78%
rename from src/com/android/contacts/model/EntityDeltaList.java
rename to src/com/android/contacts/model/RawContactDeltaList.java
index cf074ef..82dd494 100644
--- a/src/com/android/contacts/model/EntityDeltaList.java
+++ b/src/com/android/contacts/model/RawContactDeltaList.java
@@ -30,7 +30,7 @@
 import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
 
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
 import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
@@ -38,40 +38,39 @@
 import java.util.Iterator;
 
 /**
- * Container for multiple {@link EntityDelta} objects, usually when editing
+ * Container for multiple {@link RawContactDelta} objects, usually when editing
  * together as an entire aggregate. Provides convenience methods for parceling
- * and applying another {@link EntityDeltaList} over it.
+ * and applying another {@link RawContactDeltaList} over it.
  */
-public class EntityDeltaList extends ArrayList<EntityDelta> implements Parcelable {
-    private static final String TAG = "EntityDeltaList";
+public class RawContactDeltaList extends ArrayList<RawContactDelta> implements Parcelable {
+    private static final String TAG = RawContactDeltaList.class.getSimpleName();
     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
 
     private boolean mSplitRawContacts;
     private long[] mJoinWithRawContactIds;
 
-    private EntityDeltaList() {
+    private RawContactDeltaList() {
     }
 
     /**
-     * Create an {@link EntityDeltaList} that contains the given {@link EntityDelta},
+     * Create an {@link RawContactDeltaList} that contains the given {@link RawContactDelta},
      * usually when inserting a new {@link Contacts} entry.
      */
-    public static EntityDeltaList fromSingle(EntityDelta delta) {
-        final EntityDeltaList state = new EntityDeltaList();
+    public static RawContactDeltaList fromSingle(RawContactDelta delta) {
+        final RawContactDeltaList state = new RawContactDeltaList();
         state.add(delta);
         return state;
     }
 
     /**
-     * Create an {@link EntityDeltaList} based on {@link Contacts} specified by the
+     * Create an {@link RawContactDeltaList} based on {@link Contacts} specified by the
      * given query parameters. This closes the {@link EntityIterator} when
      * finished, so it doesn't subscribe to updates.
      */
-    public static EntityDeltaList fromQuery(Uri entityUri, ContentResolver resolver,
+    public static RawContactDeltaList fromQuery(Uri entityUri, ContentResolver resolver,
             String selection, String[] selectionArgs, String sortOrder) {
-        final EntityIterator iterator = RawContacts.newEntityIterator(resolver.query(
-                entityUri, null, selection, selectionArgs,
-                sortOrder));
+        final EntityIterator iterator = RawContacts.newEntityIterator(
+                resolver.query(entityUri, null, selection, selectionArgs, sortOrder));
         try {
             return fromIterator(iterator);
         } finally {
@@ -80,36 +79,41 @@
     }
 
     /**
-     * Create an {@link EntityDeltaList} that contains the entities of the Iterator as before
-     * values.
+     * Create an {@link RawContactDeltaList} that contains the entities of the Iterator as before
+     * values.  This function can be passed an iterator of Entity objects or an iterator of
+     * RawContact objects.
      */
-    public static EntityDeltaList fromIterator(Iterator<Entity> iterator) {
-        final EntityDeltaList state = new EntityDeltaList();
+    public static RawContactDeltaList fromIterator(Iterator<?> iterator) {
+        final RawContactDeltaList state = new RawContactDeltaList();
         // Perform background query to pull contact details
         while (iterator.hasNext()) {
             // Read all contacts into local deltas to prepare for edits
-            final Entity before = iterator.next();
-            final EntityDelta entity = EntityDelta.fromBefore(before);
-            state.add(entity);
+            Object nextObject = iterator.next();
+            final RawContact before = nextObject instanceof Entity
+                    ? RawContact.createFrom((Entity) nextObject)
+                    : (RawContact) nextObject;
+            final RawContactDelta rawContactDelta = RawContactDelta.fromBefore(before);
+            state.add(rawContactDelta);
         }
         return state;
     }
 
     /**
-     * Merge the "after" values from the given {@link EntityDeltaList}, discarding any
+     * Merge the "after" values from the given {@link RawContactDeltaList}, discarding any
      * previous "after" states. This is typically used when re-parenting user
-     * edits onto an updated {@link EntityDeltaList}.
+     * edits onto an updated {@link RawContactDeltaList}.
      */
-    public static EntityDeltaList mergeAfter(EntityDeltaList local, EntityDeltaList remote) {
-        if (local == null) local = new EntityDeltaList();
+    public static RawContactDeltaList mergeAfter(RawContactDeltaList local,
+            RawContactDeltaList remote) {
+        if (local == null) local = new RawContactDeltaList();
 
         // For each entity in the remote set, try matching over existing
-        for (EntityDelta remoteEntity : remote) {
+        for (RawContactDelta remoteEntity : remote) {
             final Long rawContactId = remoteEntity.getValues().getId();
 
             // Find or create local match and merge
-            final EntityDelta localEntity = local.getByRawContactId(rawContactId);
-            final EntityDelta merged = EntityDelta.mergeAfter(localEntity, remoteEntity);
+            final RawContactDelta localEntity = local.getByRawContactId(rawContactId);
+            final RawContactDelta merged = RawContactDelta.mergeAfter(localEntity, remoteEntity);
 
             if (localEntity == null && merged != null) {
                 // No local entry before, so insert
@@ -123,7 +127,7 @@
     /**
      * Build a list of {@link ContentProviderOperation} that will transform all
      * the "before" {@link Entity} states into the modified state which all
-     * {@link EntityDelta} objects represent. This method specifically creates
+     * {@link RawContactDelta} objects represent. This method specifically creates
      * any {@link AggregationExceptions} rules needed to groups edits together.
      */
     public ArrayList<ContentProviderOperation> buildDiff() {
@@ -136,7 +140,7 @@
         int firstInsertRow = -1;
 
         // First pass enforces versions remain consistent
-        for (EntityDelta delta : this) {
+        for (RawContactDelta delta : this) {
             delta.buildAssert(diff);
         }
 
@@ -146,7 +150,7 @@
         int rawContactIndex = 0;
 
         // Second pass builds actual operations
-        for (EntityDelta delta : this) {
+        for (RawContactDelta delta : this) {
             final int firstBatch = diff.size();
             final boolean isInsert = delta.isContactInsert();
             backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
@@ -281,12 +285,12 @@
     }
 
     /**
-     * Search all contained {@link EntityDelta} for the first one with an
+     * Search all contained {@link RawContactDelta} for the first one with an
      * existing {@link RawContacts#_ID} value. Usually used when creating
      * {@link AggregationExceptions} during an update.
      */
     public long findRawContactId() {
-        for (EntityDelta delta : this) {
+        for (RawContactDelta delta : this) {
             final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
             if (rawContactId != null && rawContactId >= 0) {
                 return rawContactId;
@@ -296,11 +300,11 @@
     }
 
     /**
-     * Find {@link RawContacts#_ID} of the requested {@link EntityDelta}.
+     * Find {@link RawContacts#_ID} of the requested {@link RawContactDelta}.
      */
     public Long getRawContactId(int index) {
         if (index >= 0 && index < this.size()) {
-            final EntityDelta delta = this.get(index);
+            final RawContactDelta delta = this.get(index);
             final ValuesDelta values = delta.getValues();
             if (values.isVisible()) {
                 return values.getAsLong(RawContacts._ID);
@@ -310,9 +314,9 @@
     }
 
     /**
-     * Find the raw-contact (an {@link EntityDelta}) with the specified ID.
+     * Find the raw-contact (an {@link RawContactDelta}) with the specified ID.
      */
-    public EntityDelta getByRawContactId(Long rawContactId) {
+    public RawContactDelta getByRawContactId(Long rawContactId) {
         final int index = this.indexOfRawContactId(rawContactId);
         return (index == -1) ? null : this.get(index);
     }
@@ -332,19 +336,21 @@
         return -1;
     }
 
-    /** Return the index of the first EntityDelta corresponding to a writable raw-contact, or -1. */
+    /**
+     * Return the index of the first RawContactDelta corresponding to a writable raw-contact, or -1.
+     * */
     public int indexOfFirstWritableRawContact(Context context) {
         // Find the first writable entity.
         int entityIndex = 0;
-        for (EntityDelta delta : this) {
+        for (RawContactDelta delta : this) {
             if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex;
             entityIndex++;
         }
         return -1;
     }
 
-    /**  Return the first EntityDelta corresponding to a writable raw-contact, or null. */
-    public EntityDelta getFirstWritableRawContact(Context context) {
+    /**  Return the first RawContactDelta corresponding to a writable raw-contact, or null. */
+    public RawContactDelta getFirstWritableRawContact(Context context) {
         final int index = indexOfFirstWritableRawContact(context);
         return (index == -1) ? null : get(index);
     }
@@ -352,7 +358,7 @@
     public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
         ValuesDelta primary = null;
         ValuesDelta randomEntry = null;
-        for (EntityDelta delta : this) {
+        for (RawContactDelta delta : this) {
             final ArrayList<ValuesDelta> mimeEntries = delta.getMimeEntries(mimeType);
             if (mimeEntries == null) return null;
 
@@ -404,7 +410,7 @@
     public void writeToParcel(Parcel dest, int flags) {
         final int size = this.size();
         dest.writeInt(size);
-        for (EntityDelta delta : this) {
+        for (RawContactDelta delta : this) {
             dest.writeParcelable(delta, flags);
         }
         dest.writeLongArray(mJoinWithRawContactIds);
@@ -416,24 +422,24 @@
         final ClassLoader loader = getClass().getClassLoader();
         final int size = source.readInt();
         for (int i = 0; i < size; i++) {
-            this.add(source.<EntityDelta> readParcelable(loader));
+            this.add(source.<RawContactDelta> readParcelable(loader));
         }
         mJoinWithRawContactIds = source.createLongArray();
         mSplitRawContacts = source.readInt() != 0;
     }
 
-    public static final Parcelable.Creator<EntityDeltaList> CREATOR =
-            new Parcelable.Creator<EntityDeltaList>() {
+    public static final Parcelable.Creator<RawContactDeltaList> CREATOR =
+            new Parcelable.Creator<RawContactDeltaList>() {
         @Override
-        public EntityDeltaList createFromParcel(Parcel in) {
-            final EntityDeltaList state = new EntityDeltaList();
+        public RawContactDeltaList createFromParcel(Parcel in) {
+            final RawContactDeltaList state = new RawContactDeltaList();
             state.readFromParcel(in);
             return state;
         }
 
         @Override
-        public EntityDeltaList[] newArray(int size) {
-            return new EntityDeltaList[size];
+        public RawContactDeltaList[] newArray(int size) {
+            return new RawContactDeltaList[size];
         }
     };
 
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/RawContactModifier.java
similarity index 91%
rename from src/com/android/contacts/model/EntityModifier.java
rename to src/com/android/contacts/model/RawContactModifier.java
index a26ceca..c397e5f 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/RawContactModifier.java
@@ -49,10 +49,14 @@
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.editor.EventFieldEditorView;
 import com.android.contacts.editor.PhoneticNameEditorView;
-import com.android.contacts.model.AccountType.EditField;
-import com.android.contacts.model.AccountType.EditType;
-import com.android.contacts.model.AccountType.EventEditType;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.RawContactDelta.ValuesDelta;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountType.EditField;
+import com.android.contacts.model.account.AccountType.EditType;
+import com.android.contacts.model.account.AccountType.EventEditType;
+import com.android.contacts.model.account.GoogleAccountType;
+import com.android.contacts.model.dataitem.DataKind;
+import com.android.contacts.model.dataitem.StructuredNameDataItem;
 import com.android.contacts.util.DateUtils;
 import com.android.contacts.util.NameConverter;
 
@@ -68,21 +72,21 @@
 import java.util.Set;
 
 /**
- * Helper methods for modifying an {@link EntityDelta}, such as inserting
+ * Helper methods for modifying an {@link RawContactDelta}, such as inserting
  * new rows, or enforcing {@link AccountType}.
  */
-public class EntityModifier {
-    private static final String TAG = "EntityModifier";
+public class RawContactModifier {
+    private static final String TAG = RawContactModifier.class.getSimpleName();
 
     /** Set to true in order to view logs on entity operations */
     private static final boolean DEBUG = false;
 
     /**
-     * For the given {@link EntityDelta}, determine if the given
+     * For the given {@link RawContactDelta}, determine if the given
      * {@link DataKind} could be inserted under specific
      * {@link AccountType}.
      */
-    public static boolean canInsert(EntityDelta state, DataKind kind) {
+    public static boolean canInsert(RawContactDelta state, DataKind kind) {
         // Insert possible when have valid types and under overall maximum
         final int visibleCount = state.getMimeEntriesCount(kind.mimeType, true);
         final boolean validTypes = hasValidTypes(state, kind);
@@ -91,8 +95,8 @@
         return (validTypes && validOverall);
     }
 
-    public static boolean hasValidTypes(EntityDelta state, DataKind kind) {
-        if (EntityModifier.hasEditTypes(kind)) {
+    public static boolean hasValidTypes(RawContactDelta state, DataKind kind) {
+        if (RawContactModifier.hasEditTypes(kind)) {
             return (getValidTypes(state, kind).size() > 0);
         } else {
             return true;
@@ -101,12 +105,12 @@
 
     /**
      * Ensure that at least one of the given {@link DataKind} exists in the
-     * given {@link EntityDelta} state, and try creating one if none exist.
+     * given {@link RawContactDelta} state, and try creating one if none exist.
      * @return The child (either newly created or the first existing one), or null if the
      *     account doesn't support this {@link DataKind}.
      */
     public static ValuesDelta ensureKindExists(
-            EntityDelta state, AccountType accountType, String mimeType) {
+            RawContactDelta state, AccountType accountType, String mimeType) {
         final DataKind kind = accountType.getKindForMimetype(mimeType);
         final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0;
 
@@ -127,16 +131,16 @@
     }
 
     /**
-     * For the given {@link EntityDelta} and {@link DataKind}, return the
+     * For the given {@link RawContactDelta} and {@link DataKind}, return the
      * list possible {@link EditType} options available based on
      * {@link AccountType}.
      */
-    public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind) {
+    public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind) {
         return getValidTypes(state, kind, null, true, null);
     }
 
     /**
-     * For the given {@link EntityDelta} and {@link DataKind}, return the
+     * For the given {@link RawContactDelta} and {@link DataKind}, return the
      * list possible {@link EditType} options available based on
      * {@link AccountType}.
      *
@@ -144,13 +148,13 @@
      *            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,
+    public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
             EditType forceInclude) {
         return getValidTypes(state, kind, forceInclude, true, null);
     }
 
     /**
-     * For the given {@link EntityDelta} and {@link DataKind}, return the
+     * For the given {@link RawContactDelta} and {@link DataKind}, return the
      * list possible {@link EditType} options available based on
      * {@link AccountType}.
      *
@@ -161,9 +165,9 @@
      *            {@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)}.
+     *            {@link #getTypeFrequencies(RawContactDelta, DataKind)}.
      */
-    private static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind,
+    private static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
             EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) {
         final ArrayList<EditType> validTypes = new ArrayList<EditType>();
 
@@ -197,11 +201,11 @@
 
     /**
      * Count up the frequency that each {@link EditType} appears in the given
-     * {@link EntityDelta}. The returned {@link SparseIntArray} maps from
+     * {@link RawContactDelta}. 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) {
+    private static SparseIntArray getTypeFrequencies(RawContactDelta state, DataKind kind) {
         final SparseIntArray typeCount = new SparseIntArray();
 
         // Find all entries for this kind, bailing early if none found
@@ -297,7 +301,7 @@
      * first primary type that doesn't already exist. When all valid types
      * exist, we pick the last valid option.
      */
-    public static EditType getBestValidType(EntityDelta state, DataKind kind,
+    public static EditType getBestValidType(RawContactDelta state, DataKind kind,
             boolean includeSecondary, int exactValue) {
         // Shortcut when no types
         if (kind.typeColumn == null) return null;
@@ -338,10 +342,10 @@
 
     /**
      * 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, int)}.
+     * {@link RawContactDelta}. Tries using the best {@link EditType} found using
+     * {@link #getBestValidType(RawContactDelta, DataKind, boolean, int)}.
      */
-    public static ValuesDelta insertChild(EntityDelta state, DataKind kind) {
+    public static ValuesDelta insertChild(RawContactDelta state, DataKind kind) {
         // First try finding a valid primary
         EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE);
         if (bestType == null) {
@@ -353,9 +357,9 @@
 
     /**
      * Insert a new child of kind {@link DataKind} into the given
-     * {@link EntityDelta}, marked with the given {@link EditType}.
+     * {@link RawContactDelta}, marked with the given {@link EditType}.
      */
-    public static ValuesDelta insertChild(EntityDelta state, DataKind kind, EditType type) {
+    public static ValuesDelta insertChild(RawContactDelta state, DataKind kind, EditType type) {
         // Bail early if invalid kind
         if (kind == null) return null;
         final ContentValues after = new ContentValues();
@@ -379,13 +383,13 @@
     }
 
     /**
-     * Processing to trim any empty {@link ValuesDelta} and {@link EntityDelta}
-     * from the given {@link EntityDeltaList}, assuming the given {@link AccountTypeManager}
+     * Processing to trim any empty {@link ValuesDelta} and {@link RawContactDelta}
+     * from the given {@link RawContactDeltaList}, assuming the given {@link AccountTypeManager}
      * dictates the structure for various fields. This method ignores rows not
      * described by the {@link AccountType}.
      */
-    public static void trimEmpty(EntityDeltaList set, AccountTypeManager accountTypes) {
-        for (EntityDelta state : set) {
+    public static void trimEmpty(RawContactDeltaList set, AccountTypeManager accountTypes) {
+        for (RawContactDelta state : set) {
             ValuesDelta values = state.getValues();
             final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
             final String dataSet = values.getAsString(RawContacts.DATA_SET);
@@ -394,12 +398,12 @@
         }
     }
 
-    public static boolean hasChanges(EntityDeltaList set, AccountTypeManager accountTypes) {
+    public static boolean hasChanges(RawContactDeltaList set, AccountTypeManager accountTypes) {
         if (set.isMarkedForSplitting() || set.isMarkedForJoining()) {
             return true;
         }
 
-        for (EntityDelta state : set) {
+        for (RawContactDelta state : set) {
             ValuesDelta values = state.getValues();
             final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
             final String dataSet = values.getAsString(RawContacts.DATA_SET);
@@ -413,11 +417,11 @@
 
     /**
      * Processing to trim any empty {@link ValuesDelta} rows from the given
-     * {@link EntityDelta}, assuming the given {@link AccountType} dictates
+     * {@link RawContactDelta}, assuming the given {@link AccountType} dictates
      * the structure for various fields. This method ignores rows not described
      * by the {@link AccountType}.
      */
-    public static void trimEmpty(EntityDelta state, AccountType accountType) {
+    public static void trimEmpty(RawContactDelta state, AccountType accountType) {
         boolean hasValues = false;
 
         // Walk through entries for each well-known kind
@@ -440,7 +444,7 @@
                 final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType);
                 final boolean isGooglePhoto = isPhoto && isGoogleAccount;
 
-                if (EntityModifier.isEmpty(entry, kind) && !isGooglePhoto) {
+                if (RawContactModifier.isEmpty(entry, kind) && !isGooglePhoto) {
                     if (DEBUG) {
                         Log.v(TAG, "Trimming: " + entry.toString());
                     }
@@ -456,7 +460,7 @@
         }
     }
 
-    private static boolean hasChanges(EntityDelta state, AccountType accountType) {
+    private static boolean hasChanges(RawContactDelta state, AccountType accountType) {
         for (DataKind kind : accountType.getSortedDataKinds()) {
             final String mimeType = kind.mimeType;
             final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
@@ -516,10 +520,10 @@
     }
 
     /**
-     * Parse the given {@link Bundle} into the given {@link EntityDelta} state,
+     * Parse the given {@link Bundle} into the given {@link RawContactDelta} state,
      * assuming the extras defined through {@link Intents}.
      */
-    public static void parseExtras(Context context, AccountType accountType, EntityDelta state,
+    public static void parseExtras(Context context, AccountType accountType, RawContactDelta state,
             Bundle extras) {
         if (extras == null || extras.size() == 0) {
             // Bail early if no useful data
@@ -560,8 +564,8 @@
         final boolean hasOrg = extras.containsKey(Insert.COMPANY)
                 || extras.containsKey(Insert.JOB_TITLE);
         final DataKind kindOrg = accountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
-        if (hasOrg && EntityModifier.canInsert(state, kindOrg)) {
-            final ValuesDelta child = EntityModifier.insertChild(state, kindOrg);
+        if (hasOrg && RawContactModifier.canInsert(state, kindOrg)) {
+            final ValuesDelta child = RawContactModifier.insertChild(state, kindOrg);
 
             final String company = extras.getString(Insert.COMPANY);
             if (ContactsUtils.isGraphic(company)) {
@@ -577,8 +581,8 @@
         // Notes
         final boolean hasNotes = extras.containsKey(Insert.NOTES);
         final DataKind kindNotes = accountType.getKindForMimetype(Note.CONTENT_ITEM_TYPE);
-        if (hasNotes && EntityModifier.canInsert(state, kindNotes)) {
-            final ValuesDelta child = EntityModifier.insertChild(state, kindNotes);
+        if (hasNotes && RawContactModifier.canInsert(state, kindNotes)) {
+            final ValuesDelta child = RawContactModifier.insertChild(state, kindNotes);
 
             final String notes = extras.getString(Insert.NOTES);
             if (ContactsUtils.isGraphic(notes)) {
@@ -594,9 +598,9 @@
     }
 
     private static void parseStructuredNameExtra(
-            Context context, AccountType accountType, EntityDelta state, Bundle extras) {
+            Context context, AccountType accountType, RawContactDelta state, Bundle extras) {
         // StructuredName
-        EntityModifier.ensureKindExists(state, accountType, StructuredName.CONTENT_ITEM_TYPE);
+        RawContactModifier.ensureKindExists(state, accountType, StructuredName.CONTENT_ITEM_TYPE);
         final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
 
         final String name = extras.getString(Insert.NAME);
@@ -649,7 +653,7 @@
     }
 
     private static void parseStructuredPostalExtra(
-            AccountType accountType, EntityDelta state, Bundle extras) {
+            AccountType accountType, RawContactDelta state, Bundle extras) {
         // StructuredPostal
         final DataKind kind = accountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
         final ValuesDelta child = parseExtras(state, kind, extras, Insert.POSTAL_TYPE,
@@ -675,7 +679,8 @@
     }
 
     private static void parseValues(
-            EntityDelta state, AccountType accountType, ArrayList<ContentValues> dataValueList) {
+            RawContactDelta state, AccountType accountType,
+            ArrayList<ContentValues> dataValueList) {
         for (ContentValues values : dataValueList) {
             String mimeType = values.getAsString(Data.MIMETYPE);
             if (TextUtils.isEmpty(mimeType)) {
@@ -873,7 +878,7 @@
 
     /**
      * Parse a specific entry from the given {@link Bundle} and insert into the
-     * given {@link EntityDelta}. Silently skips the insert when missing value
+     * given {@link RawContactDelta}. Silently skips the insert when missing value
      * or no valid {@link EditType} found.
      *
      * @param typeExtra {@link Bundle} key that holds the incoming
@@ -881,7 +886,7 @@
      * @param valueExtra {@link Bundle} key that holds the incoming value.
      * @param valueColumn Column to write value into {@link ValuesDelta}.
      */
-    public static ValuesDelta parseExtras(EntityDelta state, DataKind kind, Bundle extras,
+    public static ValuesDelta parseExtras(RawContactDelta state, DataKind kind, Bundle extras,
             String typeExtra, String valueExtra, String valueColumn) {
         final CharSequence value = extras.getCharSequence(valueExtra);
 
@@ -889,7 +894,7 @@
         if (kind == null) return null;
 
         // Bail when can't insert type, or value missing
-        final boolean canInsert = EntityModifier.canInsert(state, kind);
+        final boolean canInsert = RawContactModifier.canInsert(state, kind);
         final boolean validValue = (value != null && TextUtils.isGraphic(value));
         if (!validValue || !canInsert) return null;
 
@@ -897,10 +902,10 @@
         final boolean hasType = extras.containsKey(typeExtra);
         final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM
                 : Integer.MIN_VALUE);
-        final EditType editType = EntityModifier.getBestValidType(state, kind, true, typeValue);
+        final EditType editType = RawContactModifier.getBestValidType(state, kind, true, typeValue);
 
         // Create data row and fill with value
-        final ValuesDelta child = EntityModifier.insertChild(state, kind, editType);
+        final ValuesDelta child = RawContactModifier.insertChild(state, kind, editType);
         child.put(valueColumn, value.toString());
 
         if (editType != null && editType.customColumn != null) {
@@ -937,13 +942,13 @@
     private static final int TYPE_CUSTOM = Phone.TYPE_CUSTOM;
 
     /**
-     * Migrates old EntityDelta to newly created one with a new restriction supplied from
+     * Migrates old RawContactDelta to newly created one with a new restriction supplied from
      * newAccountType.
      *
      * This is only for account switch during account creation (which must be insert operation).
      */
     public static void migrateStateForNewContact(Context context,
-            EntityDelta oldState, EntityDelta newState,
+            RawContactDelta oldState, RawContactDelta newState,
             AccountType oldAccountType, AccountType newAccountType) {
         if (newAccountType == oldAccountType) {
             // Just copying all data in oldState isn't enough, but we can still rely on a lot of
@@ -996,8 +1001,8 @@
      * Checks {@link DataKind#isList} and {@link DataKind#typeOverallMax}, and restricts
      * the number of entries (ValuesDelta) inside newState.
      */
-    private static ArrayList<ValuesDelta> ensureEntryMaxSize(EntityDelta newState, DataKind kind,
-            ArrayList<ValuesDelta> mimeEntries) {
+    private static ArrayList<ValuesDelta> ensureEntryMaxSize(RawContactDelta newState,
+            DataKind kind, ArrayList<ValuesDelta> mimeEntries) {
         if (mimeEntries == null) {
             return null;
         }
@@ -1015,7 +1020,8 @@
 
     /** @hide Public only for testing. */
     public static void migrateStructuredName(
-            Context context, EntityDelta oldState, EntityDelta newState, DataKind newDataKind) {
+            Context context, RawContactDelta oldState, RawContactDelta newState,
+            DataKind newDataKind) {
         final ContentValues values =
                 oldState.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE).getAfter();
         if (values == null) {
@@ -1071,24 +1077,24 @@
         if (!TextUtils.isEmpty(phoneticFullName)) {
             if (!supportPhoneticFullName) {
                 // Old data has a phonetic (full) name, while the new account doesn't allow it.
-                final ContentValues tmpValues =
+                final StructuredNameDataItem tmpItem =
                         PhoneticNameEditorView.parsePhoneticName(phoneticFullName, null);
                 values.remove(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
                 if (supportPhoneticFamilyName) {
                     values.put(StructuredName.PHONETIC_FAMILY_NAME,
-                            tmpValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
+                            tmpItem.getPhoneticFamilyName());
                 } else {
                     values.remove(StructuredName.PHONETIC_FAMILY_NAME);
                 }
                 if (supportPhoneticMiddleName) {
                     values.put(StructuredName.PHONETIC_MIDDLE_NAME,
-                            tmpValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME));
+                            tmpItem.getPhoneticMiddleName());
                 } else {
                     values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
                 }
                 if (supportPhoneticGivenName) {
                     values.put(StructuredName.PHONETIC_GIVEN_NAME,
-                            tmpValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
+                            tmpItem.getPhoneticGivenName());
                 } else {
                     values.remove(StructuredName.PHONETIC_GIVEN_NAME);
                 }
@@ -1117,7 +1123,7 @@
     }
 
     /** @hide Public only for testing. */
-    public static void migratePostal(EntityDelta oldState, EntityDelta newState,
+    public static void migratePostal(RawContactDelta oldState, RawContactDelta newState,
             DataKind newDataKind) {
         final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
                 oldState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
@@ -1228,7 +1234,7 @@
     }
 
     /** @hide Public only for testing. */
-    public static void migrateEvent(EntityDelta oldState, EntityDelta newState,
+    public static void migrateEvent(RawContactDelta oldState, RawContactDelta newState,
             DataKind newDataKind, Integer defaultYear) {
         final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
                 oldState.getMimeEntries(Event.CONTENT_ITEM_TYPE));
@@ -1285,7 +1291,7 @@
 
     /** @hide Public only for testing. */
     public static void migrateGenericWithoutTypeColumn(
-            EntityDelta oldState, EntityDelta newState, DataKind newDataKind) {
+            RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
         final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
                 oldState.getMimeEntries(newDataKind.mimeType));
         if (mimeEntries == null || mimeEntries.isEmpty()) {
@@ -1302,7 +1308,7 @@
 
     /** @hide Public only for testing. */
     public static void migrateGenericWithTypeColumn(
-            EntityDelta oldState, EntityDelta newState, DataKind newDataKind) {
+            RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
         final ArrayList<ValuesDelta> mimeEntries = oldState.getMimeEntries(newDataKind.mimeType);
         if (mimeEntries == null || mimeEntries.isEmpty()) {
             return;
diff --git a/src/com/android/contacts/model/AccountType.java b/src/com/android/contacts/model/account/AccountType.java
similarity index 98%
rename from src/com/android/contacts/model/AccountType.java
rename to src/com/android/contacts/model/account/AccountType.java
index f3aa868..edd17a0 100644
--- a/src/com/android/contacts/model/AccountType.java
+++ b/src/com/android/contacts/model/account/AccountType.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.account;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -28,6 +28,8 @@
 import android.widget.EditText;
 
 import com.android.contacts.R;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.dataitem.DataKind;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
diff --git a/src/com/android/contacts/model/AccountTypeWithDataSet.java b/src/com/android/contacts/model/account/AccountTypeWithDataSet.java
similarity index 98%
rename from src/com/android/contacts/model/AccountTypeWithDataSet.java
rename to src/com/android/contacts/model/account/AccountTypeWithDataSet.java
index 8d55758..ab0a891 100644
--- a/src/com/android/contacts/model/AccountTypeWithDataSet.java
+++ b/src/com/android/contacts/model/account/AccountTypeWithDataSet.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.account;
 
 import android.content.Context;
 import android.database.Cursor;
diff --git a/src/com/android/contacts/model/AccountWithDataSet.java b/src/com/android/contacts/model/account/AccountWithDataSet.java
similarity index 99%
rename from src/com/android/contacts/model/AccountWithDataSet.java
rename to src/com/android/contacts/model/account/AccountWithDataSet.java
index 2a8fac7..03fcc02 100644
--- a/src/com/android/contacts/model/AccountWithDataSet.java
+++ b/src/com/android/contacts/model/account/AccountWithDataSet.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.account;
 
 import android.accounts.Account;
 import android.content.Context;
diff --git a/src/com/android/contacts/model/BaseAccountType.java b/src/com/android/contacts/model/account/BaseAccountType.java
similarity index 99%
rename from src/com/android/contacts/model/BaseAccountType.java
rename to src/com/android/contacts/model/account/BaseAccountType.java
index f67ad53..7f9e1ef 100644
--- a/src/com/android/contacts/model/BaseAccountType.java
+++ b/src/com/android/contacts/model/account/BaseAccountType.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.account;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -39,6 +39,7 @@
 import android.view.inputmethod.EditorInfo;
 
 import com.android.contacts.R;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.test.NeededForTesting;
 import com.android.contacts.util.DateUtils;
 import com.google.common.collect.Lists;
diff --git a/src/com/android/contacts/model/ExchangeAccountType.java b/src/com/android/contacts/model/account/ExchangeAccountType.java
similarity index 99%
rename from src/com/android/contacts/model/ExchangeAccountType.java
rename to src/com/android/contacts/model/account/ExchangeAccountType.java
index 6c73568..5ca3308 100644
--- a/src/com/android/contacts/model/ExchangeAccountType.java
+++ b/src/com/android/contacts/model/account/ExchangeAccountType.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.account;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -32,6 +32,7 @@
 import android.util.Log;
 
 import com.android.contacts.R;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.DateUtils;
 import com.google.common.collect.Lists;
 
diff --git a/src/com/android/contacts/model/ExternalAccountType.java b/src/com/android/contacts/model/account/ExternalAccountType.java
similarity index 99%
rename from src/com/android/contacts/model/ExternalAccountType.java
rename to src/com/android/contacts/model/account/ExternalAccountType.java
index 3aca24f..71dbebf 100644
--- a/src/com/android/contacts/model/ExternalAccountType.java
+++ b/src/com/android/contacts/model/account/ExternalAccountType.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.account;
 
 import android.content.Context;
 import android.content.pm.PackageInfo;
@@ -31,6 +31,7 @@
 import android.util.Log;
 import android.util.Xml;
 
+import com.android.contacts.model.dataitem.DataKind;
 import com.google.common.annotations.VisibleForTesting;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/src/com/android/contacts/model/FallbackAccountType.java b/src/com/android/contacts/model/account/FallbackAccountType.java
similarity index 96%
rename from src/com/android/contacts/model/FallbackAccountType.java
rename to src/com/android/contacts/model/account/FallbackAccountType.java
index 21eb9e7..dae288d 100644
--- a/src/com/android/contacts/model/FallbackAccountType.java
+++ b/src/com/android/contacts/model/account/FallbackAccountType.java
@@ -14,12 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.account;
 
 import android.content.Context;
 import android.util.Log;
 
 import com.android.contacts.R;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.test.NeededForTesting;
 
 public class FallbackAccountType extends BaseAccountType {
diff --git a/src/com/android/contacts/model/GoogleAccountType.java b/src/com/android/contacts/model/account/GoogleAccountType.java
similarity index 98%
rename from src/com/android/contacts/model/GoogleAccountType.java
rename to src/com/android/contacts/model/account/GoogleAccountType.java
index d7360ee..192c3d0 100644
--- a/src/com/android/contacts/model/GoogleAccountType.java
+++ b/src/com/android/contacts/model/account/GoogleAccountType.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.account;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -25,6 +25,7 @@
 import android.util.Log;
 
 import com.android.contacts.R;
+import com.android.contacts.model.dataitem.DataKind;
 import com.android.contacts.util.DateUtils;
 import com.google.common.collect.Lists;
 
diff --git a/src/com/android/contacts/model/dataitem/DataItem.java b/src/com/android/contacts/model/dataitem/DataItem.java
new file mode 100644
index 0000000..25c44cb
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/DataItem.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts.Data;
+
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.RawContact;
+import com.android.contacts.model.account.AccountType;
+
+/**
+ * This is the base class for data items, which represents a row from the Data table.
+ */
+public class DataItem {
+
+    private final ContentValues mContentValues;
+
+    /**
+     * The raw contact that this data item is associated with.  This can be null.
+     */
+    private final RawContact mRawContact;
+    private DataKind mDataKind;
+
+    protected DataItem(RawContact rawContact, ContentValues values) {
+        mContentValues = values;
+        mRawContact = rawContact;
+    }
+
+    /**
+     * Factory for creating subclasses of DataItem objects based on the mimetype in the
+     * content values.  Raw contact is the raw contact that this data item is associated with.
+     */
+    public static DataItem createFrom(RawContact rawContact, ContentValues values) {
+        final String mimeType = values.getAsString(Data.MIMETYPE);
+        if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new GroupMembershipDataItem(rawContact, values);
+        } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new StructuredNameDataItem(rawContact, values);
+        } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new PhoneDataItem(rawContact, values);
+        } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new EmailDataItem(rawContact, values);
+        } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new StructuredPostalDataItem(rawContact, values);
+        } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new ImDataItem(rawContact, values);
+        } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new OrganizationDataItem(rawContact, values);
+        } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new NicknameDataItem(rawContact, values);
+        } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new NoteDataItem(rawContact, values);
+        } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new WebsiteDataItem(rawContact, values);
+        } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new SipAddressDataItem(rawContact, values);
+        } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new EventDataItem(rawContact, values);
+        } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new RelationDataItem(rawContact, values);
+        } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new IdentityDataItem(rawContact, values);
+        } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new PhotoDataItem(rawContact, values);
+        }
+
+        // generic
+        return new DataItem(rawContact, values);
+    }
+
+    public ContentValues getContentValues() {
+        return mContentValues;
+    }
+
+    protected RawContact getRawContact() {
+        return mRawContact;
+    }
+
+    public void setRawContactId(long rawContactId) {
+        mContentValues.put(Data.RAW_CONTACT_ID, rawContactId);
+    }
+
+    /**
+     * Returns the data id.
+     */
+    public long getId() {
+        return mContentValues.getAsLong(Data._ID);
+    }
+
+    public long getRawContactId() {
+        return mContentValues.getAsLong(Data.RAW_CONTACT_ID);
+    }
+    /**
+     * Returns the mimetype of the data.
+     */
+    public String getMimeType() {
+        return mContentValues.getAsString(Data.MIMETYPE);
+    }
+
+    public void setMimeType(String mimeType) {
+        mContentValues.put(Data.MIMETYPE, mimeType);
+    }
+
+    public boolean isPrimary() {
+        Integer primary = mContentValues.getAsInteger(Data.IS_PRIMARY);
+        return primary != null && primary != 0;
+    }
+
+    public boolean isSuperPrimary() {
+        Integer superPrimary = mContentValues.getAsInteger(Data.IS_SUPER_PRIMARY);
+        return superPrimary != null && superPrimary != 0;
+    }
+
+    public int getDataVersion() {
+        return mContentValues.getAsInteger(Data.DATA_VERSION);
+    }
+
+    public AccountTypeManager getAccountTypeManager() {
+        if (mRawContact == null) {
+            return null;
+        } else {
+            return mRawContact.getAccountTypeManager();
+        }
+    }
+
+    public AccountType getAccountType() {
+        if (mRawContact == null) {
+            return null;
+        } else {
+            return mRawContact.getAccountType();
+        }
+    }
+
+    /**
+     * This method can only be invoked if the raw contact is non-null.
+     */
+    public DataKind getDataKind() {
+        if (mRawContact == null) {
+            throw new IllegalStateException("mRawContact must be non-null to call getDataKind()");
+        }
+
+        if (mDataKind == null) {
+            mDataKind = getAccountTypeManager().getKindOrFallback(
+                    mRawContact.getAccountTypeString(), mRawContact.getDataSet(), getMimeType());
+        }
+
+        return mDataKind;
+    }
+
+    public boolean hasKindTypeColumn() {
+        final String key = getDataKind().typeColumn;
+        return key != null && mContentValues.containsKey(key);
+    }
+
+    public int getKindTypeColumn() {
+        final String key = getDataKind().typeColumn;
+        return mContentValues.getAsInteger(key);
+    }
+
+    /**
+     * This builds the data string depending on the type of data item by using the generic
+     * DataKind object underneath.  This DataItem object must be associated with a raw contact
+     * for this function to work.
+     */
+    public String buildDataString() {
+        if (mRawContact == null) {
+            throw new IllegalStateException("mRawContact must be non-null to call getDataKind()");
+        }
+        final DataKind kind = getDataKind();
+
+        if (kind.actionBody == null) {
+            return null;
+        }
+        CharSequence actionBody = kind.actionBody.inflateUsing(mRawContact.getContext(),
+                mContentValues);
+        return actionBody == null ? null : actionBody.toString();
+    }
+
+    public String getKindString() {
+        final DataKind kind = getDataKind();
+        return (kind.titleRes == -1 || kind.titleRes == 0) ? ""
+                : mRawContact.getContext().getString(kind.titleRes);
+    }
+}
diff --git a/src/com/android/contacts/model/DataKind.java b/src/com/android/contacts/model/dataitem/DataKind.java
similarity index 95%
rename from src/com/android/contacts/model/DataKind.java
rename to src/com/android/contacts/model/dataitem/DataKind.java
index bae9836..8707012 100644
--- a/src/com/android/contacts/model/DataKind.java
+++ b/src/com/android/contacts/model/dataitem/DataKind.java
@@ -14,15 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.contacts.model;
+package com.android.contacts.model.dataitem;
 
 import android.content.ContentValues;
 import android.provider.ContactsContract.Data;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType.EditField;
-import com.android.contacts.model.AccountType.EditType;
-import com.android.contacts.model.AccountType.StringInflater;
+import com.android.contacts.model.account.AccountType.EditField;
+import com.android.contacts.model.account.AccountType.EditType;
+import com.android.contacts.model.account.AccountType.StringInflater;
 import com.google.common.collect.Iterators;
 
 import java.text.SimpleDateFormat;
diff --git a/src/com/android/contacts/model/dataitem/EmailDataItem.java b/src/com/android/contacts/model/dataitem/EmailDataItem.java
new file mode 100644
index 0000000..a535c73
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/EmailDataItem.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents an email data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Email}.
+ */
+public class EmailDataItem extends DataItem {
+
+    /* package */ EmailDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getAddress() {
+        return getContentValues().getAsString(Email.ADDRESS);
+    }
+
+    public String getDisplayName() {
+        return getContentValues().getAsString(Email.DISPLAY_NAME);
+    }
+
+    public String getData() {
+        return getContentValues().getAsString(Email.DATA);
+    }
+
+    /**
+     * Values is one of Email.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(Email.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Email.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/EventDataItem.java b/src/com/android/contacts/model/dataitem/EventDataItem.java
new file mode 100644
index 0000000..2114279
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/EventDataItem.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents an event data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Event}.
+ */
+public class EventDataItem extends DataItem {
+
+    /* package */ EventDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getStartDate() {
+        return getContentValues().getAsString(Event.START_DATE);
+    }
+
+    /**
+     * Values are one of Event.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(Event.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Event.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/GroupMembershipDataItem.java b/src/com/android/contacts/model/dataitem/GroupMembershipDataItem.java
new file mode 100644
index 0000000..aea9bca
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/GroupMembershipDataItem.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a group memebership data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.GroupMembership}.
+ */
+public class GroupMembershipDataItem extends DataItem {
+
+    /* package */ GroupMembershipDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public long getGroupRowId() {
+        return getContentValues().getAsLong(GroupMembership.GROUP_ROW_ID);
+    }
+
+    public String getGroupSourceId() {
+        return getContentValues().getAsString(GroupMembership.GROUP_SOURCE_ID);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/IdentityDataItem.java b/src/com/android/contacts/model/dataitem/IdentityDataItem.java
new file mode 100644
index 0000000..fd4b836
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/IdentityDataItem.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents an identity data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Identity}.
+ */
+public class IdentityDataItem extends DataItem {
+
+    /* package */ IdentityDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getIdentity() {
+        return getContentValues().getAsString(Identity.IDENTITY);
+    }
+
+    public String getNamespace() {
+        return getContentValues().getAsString(Identity.NAMESPACE);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/ImDataItem.java b/src/com/android/contacts/model/dataitem/ImDataItem.java
new file mode 100644
index 0000000..3a08325
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/ImDataItem.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents an IM data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Im}.
+ */
+public class ImDataItem extends DataItem {
+
+    private final boolean mCreatedFromEmail;
+
+    /* package */ ImDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+        mCreatedFromEmail = false;
+    }
+
+    private ImDataItem(RawContact rawContact, ContentValues values,
+            boolean createdFromEmail) {
+        super(rawContact, values);
+        mCreatedFromEmail = createdFromEmail;
+    }
+
+    public static ImDataItem createFromEmail(EmailDataItem item) {
+        ImDataItem im = new ImDataItem(item.getRawContact(),
+                new ContentValues(item.getContentValues()), true);
+        im.setMimeType(Im.CONTENT_ITEM_TYPE);
+        return im;
+    }
+
+    public String getData() {
+        if (mCreatedFromEmail) {
+            return getContentValues().getAsString(Email.DATA);
+        } else {
+            return getContentValues().getAsString(Im.DATA);
+        }
+    }
+
+    /**
+     * Values are one of Im.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(Im.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Im.LABEL);
+    }
+
+    /**
+     * Values are one of Im.PROTOCOL_
+     */
+    public Integer getProtocol() {
+        return getContentValues().getAsInteger(Im.PROTOCOL);
+    }
+
+    public boolean isProtocolValid() {
+        return getProtocol() != null;
+    }
+
+    public String getCustomProtocol() {
+        return getContentValues().getAsString(Im.CUSTOM_PROTOCOL);
+    }
+
+    public int getChatCapability() {
+        Integer result = getContentValues().getAsInteger(Im.CHAT_CAPABILITY);
+        return result == null ? 0 : result;
+    }
+
+    public boolean isCreatedFromEmail() {
+        return mCreatedFromEmail;
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/NicknameDataItem.java b/src/com/android/contacts/model/dataitem/NicknameDataItem.java
new file mode 100644
index 0000000..7b52510
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/NicknameDataItem.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a nickname data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Nickname}.
+ */
+public class NicknameDataItem extends DataItem {
+
+    public NicknameDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getName() {
+        return getContentValues().getAsString(Nickname.NAME);
+    }
+
+    /**
+     * Types are defined as Nickname.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(Nickname.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Nickname.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/NoteDataItem.java b/src/com/android/contacts/model/dataitem/NoteDataItem.java
new file mode 100644
index 0000000..0d0fa24
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/NoteDataItem.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a note data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Note}.
+ */
+public class NoteDataItem extends DataItem {
+
+    /* package */ NoteDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getNote() {
+        return getContentValues().getAsString(Note.NOTE);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/OrganizationDataItem.java b/src/com/android/contacts/model/dataitem/OrganizationDataItem.java
new file mode 100644
index 0000000..1326bdb
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/OrganizationDataItem.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents an organization data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Organization}.
+ */
+public class OrganizationDataItem extends DataItem {
+
+    /* package */ OrganizationDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getCompany() {
+        return getContentValues().getAsString(Organization.COMPANY);
+    }
+
+    /**
+     * Values are one of Organization.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(Organization.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Organization.LABEL);
+    }
+
+    public String getTitle() {
+        return getContentValues().getAsString(Organization.TITLE);
+    }
+
+    public String getDepartment() {
+        return getContentValues().getAsString(Organization.DEPARTMENT);
+    }
+
+    public String getJobDescription() {
+        return getContentValues().getAsString(Organization.JOB_DESCRIPTION);
+    }
+
+    public String getSymbol() {
+        return getContentValues().getAsString(Organization.SYMBOL);
+    }
+
+    public String getPhoneticName() {
+        return getContentValues().getAsString(Organization.PHONETIC_NAME);
+    }
+
+    public String getOfficeLocation() {
+        return getContentValues().getAsString(Organization.OFFICE_LOCATION);
+    }
+
+    public String getPhoneticNameStyle() {
+        return getContentValues().getAsString(Organization.PHONETIC_NAME_STYLE);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/PhoneDataItem.java b/src/com/android/contacts/model/dataitem/PhoneDataItem.java
new file mode 100644
index 0000000..94a0054
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/PhoneDataItem.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a phone data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Phone}.
+ */
+public class PhoneDataItem extends DataItem {
+
+    /* package */ PhoneDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getNumber() {
+        return getContentValues().getAsString(Phone.NUMBER);
+    }
+
+    /**
+     * Returns the normalized phone number in E164 format.
+     */
+    public String getNormalizedNumber() {
+        return getContentValues().getAsString(Phone.NORMALIZED_NUMBER);
+    }
+
+    /**
+     * Values are Phone.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(Phone.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Phone.LABEL);
+    }
+
+}
diff --git a/src/com/android/contacts/model/dataitem/PhotoDataItem.java b/src/com/android/contacts/model/dataitem/PhotoDataItem.java
new file mode 100644
index 0000000..5e355fa
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/PhotoDataItem.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts.Photo;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a photo data item, wrapping the columns in
+ * {@link ContactsContract.Contacts.Photo}.
+ */
+public class PhotoDataItem extends DataItem {
+
+    /* package */ PhotoDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public long getPhotoFileId() {
+        return getContentValues().getAsLong(Photo.PHOTO_FILE_ID);
+    }
+
+    public byte[] getPhoto() {
+        return getContentValues().getAsByteArray(Photo.PHOTO);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/RelationDataItem.java b/src/com/android/contacts/model/dataitem/RelationDataItem.java
new file mode 100644
index 0000000..7c22cf5
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/RelationDataItem.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a relation data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Relation}.
+ */
+public class RelationDataItem extends DataItem {
+
+    /* package */ RelationDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getName() {
+        return getContentValues().getAsString(Relation.NAME);
+    }
+
+    /**
+     * Values are one of Relation.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(Relation.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Relation.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/SipAddressDataItem.java b/src/com/android/contacts/model/dataitem/SipAddressDataItem.java
new file mode 100644
index 0000000..6b8e93d
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/SipAddressDataItem.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a sip address data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.SipAddress}.
+ */
+public class SipAddressDataItem extends DataItem {
+
+    /* package */ SipAddressDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getSipAddress() {
+        return getContentValues().getAsString(SipAddress.SIP_ADDRESS);
+    }
+
+    /**
+     * Value is one of SipAddress.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(SipAddress.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(SipAddress.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/StructuredNameDataItem.java b/src/com/android/contacts/model/dataitem/StructuredNameDataItem.java
new file mode 100644
index 0000000..4654a6f
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/StructuredNameDataItem.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a structured name data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.StructuredName}.
+ */
+public class StructuredNameDataItem extends DataItem {
+
+    public StructuredNameDataItem() {
+        super(null, new ContentValues());
+        getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+    }
+
+    /* package */ StructuredNameDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getDisplayName() {
+        return getContentValues().getAsString(StructuredName.DISPLAY_NAME);
+    }
+
+    public void setDisplayName(String name) {
+        getContentValues().put(StructuredName.DISPLAY_NAME, name);
+    }
+
+    public String getGivenName() {
+        return getContentValues().getAsString(StructuredName.GIVEN_NAME);
+    }
+
+    public String getFamilyName() {
+        return getContentValues().getAsString(StructuredName.FAMILY_NAME);
+    }
+
+    public String getPrefix() {
+        return getContentValues().getAsString(StructuredName.PREFIX);
+    }
+
+    public String getMiddleName() {
+        return getContentValues().getAsString(StructuredName.MIDDLE_NAME);
+    }
+
+    public String getSuffix() {
+        return getContentValues().getAsString(StructuredName.SUFFIX);
+    }
+
+    public String getPhoneticGivenName() {
+        return getContentValues().getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+    }
+
+    public String getPhoneticMiddleName() {
+        return getContentValues().getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+    }
+
+    public String getPhoneticFamilyName() {
+        return getContentValues().getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+    }
+
+    public String getFullNameStyle() {
+        return getContentValues().getAsString(StructuredName.FULL_NAME_STYLE);
+    }
+
+    public String getPhoneticNameStyle() {
+        return getContentValues().getAsString(StructuredName.PHONETIC_NAME_STYLE);
+    }
+
+    public void setPhoneticFamilyName(String name) {
+        getContentValues().put(StructuredName.PHONETIC_FAMILY_NAME, name);
+    }
+
+    public void setPhoneticMiddleName(String name) {
+        getContentValues().put(StructuredName.PHONETIC_MIDDLE_NAME, name);
+    }
+
+    public void setPhoneticGivenName(String name) {
+        getContentValues().put(StructuredName.PHONETIC_GIVEN_NAME, name);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/StructuredPostalDataItem.java b/src/com/android/contacts/model/dataitem/StructuredPostalDataItem.java
new file mode 100644
index 0000000..cc2cf56
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/StructuredPostalDataItem.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a structured postal data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.StructuredPostal}.
+ */
+public class StructuredPostalDataItem extends DataItem {
+
+    /* package */ StructuredPostalDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getFormattedAddress() {
+        return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS);
+    }
+
+    /**
+     * Values are one of StructuredPostal.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(StructuredPostal.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(StructuredPostal.LABEL);
+    }
+
+    public String getStreet() {
+        return getContentValues().getAsString(StructuredPostal.STREET);
+    }
+
+    public String getPOBox() {
+        return getContentValues().getAsString(StructuredPostal.POBOX);
+    }
+
+    public String getNeighborhood() {
+        return getContentValues().getAsString(StructuredPostal.NEIGHBORHOOD);
+    }
+
+    public String getCity() {
+        return getContentValues().getAsString(StructuredPostal.CITY);
+    }
+
+    public String getRegion() {
+        return getContentValues().getAsString(StructuredPostal.REGION);
+    }
+
+    public String getPostcode() {
+        return getContentValues().getAsString(StructuredPostal.POSTCODE);
+    }
+
+    public String getCountry() {
+        return getContentValues().getAsString(StructuredPostal.COUNTRY);
+    }
+}
diff --git a/src/com/android/contacts/model/dataitem/WebsiteDataItem.java b/src/com/android/contacts/model/dataitem/WebsiteDataItem.java
new file mode 100644
index 0000000..c3aadf3
--- /dev/null
+++ b/src/com/android/contacts/model/dataitem/WebsiteDataItem.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+import com.android.contacts.model.RawContact;
+
+/**
+ * Represents a website data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Website}.
+ */
+public class WebsiteDataItem extends DataItem {
+
+    /* package */ WebsiteDataItem(RawContact rawContact, ContentValues values) {
+        super(rawContact, values);
+    }
+
+    public String getUrl() {
+        return getContentValues().getAsString(Website.URL);
+    }
+
+    /**
+     * Value is one of Website.TYPE_*
+     */
+    public int getType() {
+        return getContentValues().getAsInteger(Website.TYPE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Website.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/quickcontact/DataAction.java b/src/com/android/contacts/quickcontact/DataAction.java
index ec6bf4c..c10c338 100644
--- a/src/com/android/contacts/quickcontact/DataAction.java
+++ b/src/com/android/contacts/quickcontact/DataAction.java
@@ -17,27 +17,28 @@
 package com.android.contacts.quickcontact;
 
 import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.net.WebAddress;
-import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.SipAddress;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.provider.ContactsContract.Data;
 import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType.EditType;
-import com.android.contacts.model.DataKind;
+import com.android.contacts.model.account.AccountType.EditType;
+import com.android.contacts.model.dataitem.DataItem;
+import com.android.contacts.model.dataitem.DataKind;
+import com.android.contacts.model.dataitem.EmailDataItem;
+import com.android.contacts.model.dataitem.ImDataItem;
+import com.android.contacts.model.dataitem.PhoneDataItem;
+import com.android.contacts.model.dataitem.SipAddressDataItem;
+import com.android.contacts.model.dataitem.StructuredPostalDataItem;
+import com.android.contacts.model.dataitem.WebsiteDataItem;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.PhoneCapabilityTester;
 import com.android.contacts.util.StructuredPostalUtils;
@@ -68,51 +69,45 @@
     /**
      * Create an action from common {@link Data} elements.
      */
-    public DataAction(Context context, String mimeType, DataKind kind, long dataId,
-            ContentValues entryValues) {
+    public DataAction(Context context, DataItem item) {
         mContext = context;
-        mKind = kind;
-        mMimeType = mimeType;
+        mKind = item.getDataKind();
+        mMimeType = item.getMimeType();
 
         // Determine type for subtitle
         mSubtitle = "";
-        if (kind.typeColumn != null) {
-            if (entryValues.containsKey(kind.typeColumn)) {
-                final int typeValue = entryValues.getAsInteger(kind.typeColumn);
+        if (item.hasKindTypeColumn()) {
+            final int typeValue = item.getKindTypeColumn();
 
-                // get type string
-                for (EditType type : kind.typeList) {
-                    if (type.rawValue == typeValue) {
-                        if (type.customColumn == null) {
-                            // Non-custom type. Get its description from the resource
-                            mSubtitle = context.getString(type.labelRes);
-                        } else {
-                            // Custom type. Read it from the database
-                            mSubtitle = entryValues.getAsString(type.customColumn);
-                        }
-                        break;
+            // get type string
+            for (EditType type : item.getDataKind().typeList) {
+                if (type.rawValue == typeValue) {
+                    if (type.customColumn == null) {
+                        // Non-custom type. Get its description from the resource
+                        mSubtitle = context.getString(type.labelRes);
+                    } else {
+                        // Custom type. Read it from the database
+                        mSubtitle = item.getContentValues().getAsString(type.customColumn);
                     }
+                    break;
                 }
             }
         }
 
-        final Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
-        mIsPrimary = superPrimary != null && superPrimary != 0;
+        mIsPrimary = item.isSuperPrimary();
+        mBody = item.buildDataString();
 
-        if (mKind.actionBody != null) {
-            mBody = mKind.actionBody.inflateUsing(context, entryValues);
-        }
-
-        mDataId = dataId;
-        mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
+        mDataId = item.getId();
+        mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, mDataId);
 
         final boolean hasPhone = PhoneCapabilityTester.isPhone(mContext);
         final boolean hasSms = PhoneCapabilityTester.isSmsIntentRegistered(mContext);
 
         // Handle well-known MIME-types with special care
-        if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+        if (item instanceof PhoneDataItem) {
             if (PhoneCapabilityTester.isPhone(mContext)) {
-                final String number = entryValues.getAsString(Phone.NUMBER);
+                PhoneDataItem phone = (PhoneDataItem) item;
+                final String number = phone.getNumber();
                 if (!TextUtils.isEmpty(number)) {
 
                     final Intent phoneIntent = hasPhone ? ContactsUtils.getCallIntent(number)
@@ -124,8 +119,8 @@
                     if (hasPhone && hasSms) {
                         mIntent = phoneIntent;
                         mAlternateIntent = smsIntent;
-                        mAlternateIconRes = kind.iconAltRes;
-                        mAlternateIconDescriptionRes = kind.iconAltDescriptionRes;
+                        mAlternateIconRes = phone.getDataKind().iconAltRes;
+                        mAlternateIconDescriptionRes = phone.getDataKind().iconAltDescriptionRes;
                     } else if (hasPhone) {
                         mIntent = phoneIntent;
                     } else if (hasSms) {
@@ -133,9 +128,10 @@
                     }
                 }
             }
-        } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+        } else if (item instanceof SipAddressDataItem) {
             if (PhoneCapabilityTester.isSipPhone(mContext)) {
-                final String address = entryValues.getAsString(SipAddress.SIP_ADDRESS);
+                final SipAddressDataItem sip = (SipAddressDataItem) item;
+                final String address = sip.getSipAddress();
                 if (!TextUtils.isEmpty(address)) {
                     final Uri callUri = Uri.fromParts(Constants.SCHEME_SIP, address, null);
                     mIntent = ContactsUtils.getCallIntent(callUri);
@@ -147,26 +143,27 @@
                     // for the SIP-related intent-filters in its manifest.
                 }
             }
-        } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String address = entryValues.getAsString(Email.DATA);
+        } else if (item instanceof EmailDataItem) {
+            final EmailDataItem email = (EmailDataItem) item;
+            final String address = email.getData();
             if (!TextUtils.isEmpty(address)) {
                 final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
                 mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
             }
 
-        } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String url = entryValues.getAsString(Website.URL);
+        } else if (item instanceof WebsiteDataItem) {
+            final WebsiteDataItem website = (WebsiteDataItem) item;
+            final String url = website.getUrl();
             if (!TextUtils.isEmpty(url)) {
                 WebAddress webAddress = new WebAddress(url);
                 mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString()));
             }
 
-        } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
-                    entryValues.getAsString(Data.MIMETYPE));
-            if (isEmail || isProtocolValid(entryValues)) {
-                final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
-                        entryValues.getAsInteger(Im.PROTOCOL);
+        } else if (item instanceof ImDataItem) {
+            ImDataItem im = (ImDataItem) item;
+            final boolean isEmail = im.isCreatedFromEmail();
+            if (isEmail || im.isProtocolValid()) {
+                final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
 
                 if (isEmail) {
                     // Use Google Talk string when using Email, and clear data
@@ -176,8 +173,8 @@
                     mDataUri = null;
                 }
 
-                String host = entryValues.getAsString(Im.CUSTOM_PROTOCOL);
-                String data = entryValues.getAsString(isEmail ? Email.DATA : Im.DATA);
+                String host = im.getCustomProtocol();
+                String data = im.getData();
                 if (protocol != Im.PROTOCOL_CUSTOM) {
                     // Try bringing in a well-known host for specific protocols
                     host = ContactsUtils.lookupProviderNameFromId(protocol);
@@ -191,8 +188,7 @@
 
                     // If the address is also available for a video chat, we'll show the capability
                     // as a secondary action.
-                    final Integer chatCapabilityObj = entryValues.getAsInteger(Im.CHAT_CAPABILITY);
-                    final int chatCapability = chatCapabilityObj == null ? 0 : chatCapabilityObj;
+                    final int chatCapability = im.getChatCapability();
                     final boolean isVideoChatCapable =
                             (chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0;
                     final boolean isAudioChatCapable =
@@ -210,9 +206,9 @@
                     }
                 }
             }
-        } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
-            final String postalAddress =
-                    entryValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+        } else if (item instanceof StructuredPostalDataItem) {
+            StructuredPostalDataItem postal = (StructuredPostalDataItem) item;
+            final String postalAddress = postal.getFormattedAddress();
             if (!TextUtils.isEmpty(postalAddress)) {
                 mIntent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress);
             }
@@ -221,7 +217,7 @@
         if (mIntent == null) {
             // Otherwise fall back to default VIEW action
             mIntent = new Intent(Intent.ACTION_VIEW);
-            mIntent.setDataAndType(mDataUri, mimeType);
+            mIntent.setDataAndType(mDataUri, item.getMimeType());
         }
 
         mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
@@ -236,19 +232,6 @@
         mPresence = presence;
     }
 
-    private boolean isProtocolValid(ContentValues entryValues) {
-        final String protocol = entryValues.getAsString(Im.PROTOCOL);
-        if (protocol == null) {
-            return false;
-        }
-        try {
-            Integer.valueOf(protocol);
-        } catch (NumberFormatException e) {
-            return false;
-        }
-        return true;
-    }
-
     @Override
     public CharSequence getSubtitle() {
         return mSubtitle;
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 4587f1d..25fb3f4 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -22,10 +22,7 @@
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ActivityNotFoundException;
 import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Context;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
 import android.content.Intent;
 import android.content.Loader;
 import android.content.pm.PackageManager;
@@ -35,13 +32,11 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.QuickContact;
 import android.provider.ContactsContract.RawContacts;
 import android.support.v13.app.FragmentPagerAdapter;
@@ -62,10 +57,14 @@
 import android.widget.Toast;
 
 import com.android.contacts.Collapser;
-import com.android.contacts.ContactLoader;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.DataKind;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.ContactLoader;
+import com.android.contacts.model.RawContact;
+import com.android.contacts.model.dataitem.DataItem;
+import com.android.contacts.model.dataitem.DataKind;
+import com.android.contacts.model.dataitem.EmailDataItem;
+import com.android.contacts.model.dataitem.ImDataItem;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.ImageViewDrawableSetter;
@@ -330,7 +329,7 @@
     /**
      * Handle the result from the ContactLoader
      */
-    private void bindData(ContactLoader.Result data) {
+    private void bindData(Contact data) {
         final ResolveCache cache = ResolveCache.getInstance(this);
         final Context context = this;
 
@@ -339,42 +338,29 @@
 
         mDefaultsMap.clear();
 
-        mStopWatch.lap("atm"); // AccountTypeManager initialization start
-        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
-                context.getApplicationContext());
-        mStopWatch.lap("fatm"); // AccountTypeManager initialization finished
+        mStopWatch.lap("sph"); // Start photo setting
 
         final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
         mPhotoSetter.setupContactPhoto(data, photoView);
 
         mStopWatch.lap("ph"); // Photo set
 
-        for (Entity entity : data.getEntities()) {
-            final ContentValues entityValues = entity.getEntityValues();
-            final String accountType = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
-            final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
-            for (NamedContentValues subValue : entity.getSubValues()) {
-                final ContentValues entryValues = subValue.values;
-                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+        for (RawContact rawContact : data.getRawContacts()) {
+            for (DataItem dataItem : rawContact.getDataItems()) {
+                final String mimeType = dataItem.getMimeType();
 
                 // Skip this data item if MIME-type excluded
                 if (isMimeExcluded(mimeType)) continue;
 
-                final long dataId = entryValues.getAsLong(Data._ID);
-                final Integer primary = entryValues.getAsInteger(Data.IS_PRIMARY);
-                final boolean isPrimary = primary != null && primary != 0;
-                final Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
-                final boolean isSuperPrimary = superPrimary != null && superPrimary != 0;
+                final long dataId = dataItem.getId();
+                final boolean isPrimary = dataItem.isPrimary();
+                final boolean isSuperPrimary = dataItem.isSuperPrimary();
 
-                final DataKind kind =
-                        accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
-
-                if (kind != null) {
+                if (dataItem.getDataKind() != null) {
                     // Build an action for this data entry, find a mapping to a UI
                     // element, build its summary from the cursor, and collect it
                     // along with all others of this MIME-type.
-                    final Action action = new DataAction(context, mimeType, kind, dataId,
-                            entryValues);
+                    final Action action = new DataAction(context, dataItem);
                     final boolean wasAdded = considerAdd(action, cache, isSuperPrimary);
                     if (wasAdded) {
                         // Remember the default
@@ -386,12 +372,11 @@
 
                 // Handle Email rows with presence data as Im entry
                 final DataStatus status = data.getStatuses().get(dataId);
-                if (status != null && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
-                            Im.CONTENT_ITEM_TYPE);
-                    if (imKind != null) {
-                        final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE,
-                                imKind, dataId, entryValues);
+                if (status != null && dataItem instanceof EmailDataItem) {
+                    final EmailDataItem email = (EmailDataItem) dataItem;
+                    final ImDataItem im = ImDataItem.createFromEmail(email);
+                    if (im.getDataKind() != null) {
+                        final DataAction action = new DataAction(context, im);
                         action.setPresence(status.getPresence());
                         considerAdd(action, cache, isSuperPrimary);
                     }
@@ -505,14 +490,14 @@
         listFragment.setListener(mListFragmentListener);
     }
 
-    private LoaderCallbacks<ContactLoader.Result> mLoaderCallbacks =
-            new LoaderCallbacks<ContactLoader.Result>() {
+    private LoaderCallbacks<Contact> mLoaderCallbacks =
+            new LoaderCallbacks<Contact>() {
         @Override
-        public void onLoaderReset(Loader<ContactLoader.Result> loader) {
+        public void onLoaderReset(Loader<Contact> loader) {
         }
 
         @Override
-        public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
+        public void onLoadFinished(Loader<Contact> loader, Contact data) {
             mStopWatch.lap("lf"); // onLoadFinished
             if (isFinishing()) {
                 close(false);
@@ -558,7 +543,7 @@
         }
 
         @Override
-        public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
+        public Loader<Contact> onCreateLoader(int id, Bundle args) {
             if (mLookupUri == null) {
                 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early");
             }
diff --git a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
index b80169b..03517d4 100644
--- a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
+++ b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
@@ -37,10 +37,11 @@
 import android.view.View;
 import android.widget.RemoteViews;
 
-import com.android.contacts.ContactLoader;
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.ContactLoader;
+import com.android.contacts.model.account.AccountType;
 import com.android.contacts.quickcontact.QuickContactBroadcastReceiver;
 import com.android.contacts.util.ContactBadgeUtil;
 import com.android.contacts.util.HtmlUtils;
@@ -115,10 +116,10 @@
         final ContactLoader contactLoader = new ContactLoader(context, contactUri, false, true,
                 false, true);
         contactLoader.registerListener(0,
-                new ContactLoader.OnLoadCompleteListener<ContactLoader.Result>() {
+                new ContactLoader.OnLoadCompleteListener<Contact>() {
                     @Override
-                    public void onLoadComplete(Loader<ContactLoader.Result> loader,
-                            ContactLoader.Result contactData) {
+                    public void onLoadComplete(Loader<Contact> loader,
+                            Contact contactData) {
                         bindRemoteViews(context, widgetId, appWidgetManager, contactData);
                     }
                 });
@@ -127,7 +128,7 @@
     }
 
     private static void bindRemoteViews(final Context context, final int widgetId,
-            final AppWidgetManager widgetManager, ContactLoader.Result contactData) {
+            final AppWidgetManager widgetManager, Contact contactData) {
         Log.d(TAG, "Loaded " + contactData.getLookupKey()
                 + " for widget with id=" + widgetId);
         final RemoteViews views = new RemoteViews(context.getPackageName(),
diff --git a/src/com/android/contacts/util/AccountPromptUtils.java b/src/com/android/contacts/util/AccountPromptUtils.java
index 8c8c305..cdefda0 100644
--- a/src/com/android/contacts/util/AccountPromptUtils.java
+++ b/src/com/android/contacts/util/AccountPromptUtils.java
@@ -30,7 +30,7 @@
 import android.util.Log;
 
 import com.android.contacts.R;
-import com.android.contacts.model.GoogleAccountType;
+import com.android.contacts.model.account.GoogleAccountType;
 
 import java.io.IOException;
 
diff --git a/src/com/android/contacts/util/AccountSelectionUtil.java b/src/com/android/contacts/util/AccountSelectionUtil.java
index 312dd16..d83cb41 100644
--- a/src/com/android/contacts/util/AccountSelectionUtil.java
+++ b/src/com/android/contacts/util/AccountSelectionUtil.java
@@ -31,9 +31,9 @@
 import android.widget.TextView;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
 
 import java.util.List;
 
diff --git a/src/com/android/contacts/util/AccountsListAdapter.java b/src/com/android/contacts/util/AccountsListAdapter.java
index 5f17cee..4355cba 100644
--- a/src/com/android/contacts/util/AccountsListAdapter.java
+++ b/src/com/android/contacts/util/AccountsListAdapter.java
@@ -26,9 +26,9 @@
 import android.widget.TextView;
 
 import com.android.contacts.R;
-import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountType;
+import com.android.contacts.model.account.AccountWithDataSet;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/src/com/android/contacts/util/ImageViewDrawableSetter.java b/src/com/android/contacts/util/ImageViewDrawableSetter.java
index 4c7869b..b231572 100644
--- a/src/com/android/contacts/util/ImageViewDrawableSetter.java
+++ b/src/com/android/contacts/util/ImageViewDrawableSetter.java
@@ -26,8 +26,8 @@
 import android.util.Log;
 import android.widget.ImageView;
 
-import com.android.contacts.ContactLoader.Result;
 import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.model.Contact;
 
 import java.util.Arrays;
 
@@ -49,7 +49,7 @@
         mTarget = target;
     }
 
-    public void setupContactPhoto(Result contactData, ImageView photoView) {
+    public void setupContactPhoto(Contact contactData, ImageView photoView) {
         setTarget(photoView);
         setCompressedImage(contactData.getPhotoBinaryData());
     }
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
index 48d6e83..a9566ef 100644
--- a/src/com/android/contacts/vcard/ImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -49,7 +49,7 @@
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountSelectionUtil;
 import com.android.vcard.VCardEntryCounter;
 import com.android.vcard.VCardParser;
diff --git a/src/com/android/contacts/vcard/NfcImportVCardActivity.java b/src/com/android/contacts/vcard/NfcImportVCardActivity.java
index f427358..035be60 100644
--- a/src/com/android/contacts/vcard/NfcImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/NfcImportVCardActivity.java
@@ -33,7 +33,7 @@
 
 import com.android.contacts.R;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.vcard.VCardEntry;
 import com.android.vcard.VCardEntryCounter;
 import com.android.vcard.VCardParser;
diff --git a/src/com/android/contacts/vcard/SelectAccountActivity.java b/src/com/android/contacts/vcard/SelectAccountActivity.java
index 1226385..0e9c5a8 100644
--- a/src/com/android/contacts/vcard/SelectAccountActivity.java
+++ b/src/com/android/contacts/vcard/SelectAccountActivity.java
@@ -24,7 +24,7 @@
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
 import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.account.AccountWithDataSet;
 import com.android.contacts.util.AccountSelectionUtil;
 
 import java.util.List;