Move ContactLoader related code to ContactsCommon

This CL simply moves classes from Contacts into ContactsCommon.

This is needed so that Dialer can use ContactLoader related code
for b/11294679. A ContactLoader will also be needed in the future
to allow InCallUI to download hi-res photos while in call.

Bug: 11294679
Change-Id: If56a60aed2003ac7b8fcedac7ce4f1a7503bce94
diff --git a/src/com/android/contacts/common/ContactsUtils.java b/src/com/android/contacts/common/ContactsUtils.java
new file mode 100644
index 0000000..038ec26
--- /dev/null
+++ b/src/com/android/contacts/common/ContactsUtils.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.DisplayPhoto;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.test.NeededForTesting;
+import com.android.contacts.common.model.AccountTypeManager;
+
+import java.util.List;
+
+public class ContactsUtils {
+    private static final String TAG = "ContactsUtils";
+
+    private static int sThumbnailSize = -1;
+
+    // TODO find a proper place for the canonical version of these
+    public interface ProviderNames {
+        String YAHOO = "Yahoo";
+        String GTALK = "GTalk";
+        String MSN = "MSN";
+        String ICQ = "ICQ";
+        String AIM = "AIM";
+        String XMPP = "XMPP";
+        String JABBER = "JABBER";
+        String SKYPE = "SKYPE";
+        String QQ = "QQ";
+    }
+
+    /**
+     * This looks up the provider name defined in
+     * ProviderNames from the predefined IM protocol id.
+     * This is used for interacting with the IM application.
+     *
+     * @param protocol the protocol ID
+     * @return the provider name the IM app uses for the given protocol, or null if no
+     * provider is defined for the given protocol
+     * @hide
+     */
+    public static String lookupProviderNameFromId(int protocol) {
+        switch (protocol) {
+            case Im.PROTOCOL_GOOGLE_TALK:
+                return ProviderNames.GTALK;
+            case Im.PROTOCOL_AIM:
+                return ProviderNames.AIM;
+            case Im.PROTOCOL_MSN:
+                return ProviderNames.MSN;
+            case Im.PROTOCOL_YAHOO:
+                return ProviderNames.YAHOO;
+            case Im.PROTOCOL_ICQ:
+                return ProviderNames.ICQ;
+            case Im.PROTOCOL_JABBER:
+                return ProviderNames.JABBER;
+            case Im.PROTOCOL_SKYPE:
+                return ProviderNames.SKYPE;
+            case Im.PROTOCOL_QQ:
+                return ProviderNames.QQ;
+        }
+        return null;
+    }
+
+    /**
+     * Test if the given {@link CharSequence} contains any graphic characters,
+     * first checking {@link TextUtils#isEmpty(CharSequence)} to handle null.
+     */
+    public static boolean isGraphic(CharSequence str) {
+        return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str);
+    }
+
+    /**
+     * Returns true if two objects are considered equal.  Two null references are equal here.
+     */
+    @NeededForTesting
+    public static boolean areObjectsEqual(Object a, Object b) {
+        return a == b || (a != null && a.equals(b));
+    }
+
+    /**
+     * Returns true if two {@link Intent}s are both null, or have the same action.
+     */
+    public static final boolean areIntentActionEqual(Intent a, Intent b) {
+        if (a == b) {
+            return true;
+        }
+        if (a == null || b == null) {
+            return false;
+        }
+        return TextUtils.equals(a.getAction(), b.getAction());
+    }
+
+    public static boolean areContactWritableAccountsAvailable(Context context) {
+        final List<AccountWithDataSet> accounts =
+                AccountTypeManager.getInstance(context).getAccounts(true /* writeable */);
+        return !accounts.isEmpty();
+    }
+
+    public static boolean areGroupWritableAccountsAvailable(Context context) {
+        final List<AccountWithDataSet> accounts =
+                AccountTypeManager.getInstance(context).getGroupWritableAccounts();
+        return !accounts.isEmpty();
+    }
+
+    /**
+     * Returns the size (width and height) of thumbnail pictures as configured in the provider. This
+     * can safely be called from the UI thread, as the provider can serve this without performing
+     * a database access
+     */
+    public static int getThumbnailSize(Context context) {
+        if (sThumbnailSize == -1) {
+            final Cursor c = context.getContentResolver().query(
+                    DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+                    new String[] { DisplayPhoto.THUMBNAIL_MAX_DIM }, null, null, null);
+            try {
+                c.moveToFirst();
+                sThumbnailSize = c.getInt(0);
+            } finally {
+                c.close();
+            }
+        }
+        return sThumbnailSize;
+    }
+
+}
diff --git a/src/com/android/contacts/common/GroupMetaData.java b/src/com/android/contacts/common/GroupMetaData.java
new file mode 100644
index 0000000..fa86ae2
--- /dev/null
+++ b/src/com/android/contacts/common/GroupMetaData.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010 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.common;
+
+/**
+ * Meta-data for a contact group.  We load all groups associated with the contact's
+ * constituent accounts.
+ */
+public final class GroupMetaData {
+    private String mAccountName;
+    private String mAccountType;
+    private String mDataSet;
+    private long mGroupId;
+    private String mTitle;
+    private boolean mDefaultGroup;
+    private boolean mFavorites;
+
+    public GroupMetaData(String accountName, String accountType, String dataSet, long groupId,
+            String title, boolean defaultGroup, boolean favorites) {
+        this.mAccountName = accountName;
+        this.mAccountType = accountType;
+        this.mDataSet = dataSet;
+        this.mGroupId = groupId;
+        this.mTitle = title;
+        this.mDefaultGroup = defaultGroup;
+        this.mFavorites = favorites;
+    }
+
+    public String getAccountName() {
+        return mAccountName;
+    }
+
+    public String getAccountType() {
+        return mAccountType;
+    }
+
+    public String getDataSet() {
+        return mDataSet;
+    }
+
+    public long getGroupId() {
+        return mGroupId;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public boolean isDefaultGroup() {
+        return mDefaultGroup;
+    }
+
+    public boolean isFavorites() {
+        return mFavorites;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/model/Contact.java b/src/com/android/contacts/common/model/Contact.java
new file mode 100644
index 0000000..d5ff0a3
--- /dev/null
+++ b/src/com/android/contacts/common/model/Contact.java
@@ -0,0 +1,475 @@
+/*
+ * 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.common.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.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.util.DataStatus;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * 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 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;
+        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;
+        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;
+        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;
+    }
+
+    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(context);
+            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 = rawContact.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;
+    }
+}
diff --git a/src/com/android/contacts/common/model/ContactLoader.java b/src/com/android/contacts/common/model/ContactLoader.java
new file mode 100644
index 0000000..ce177b0
--- /dev/null
+++ b/src/com/android/contacts/common/model/ContactLoader.java
@@ -0,0 +1,970 @@
+/*
+ * Copyright (C) 2010 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.common.model;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.ContactLoaderUtils;
+import com.android.contacts.common.util.DataStatus;
+import com.android.contacts.common.util.UriUtils;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.PhotoDataItem;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+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<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 Contact sCachedResult = null;
+
+    private final Uri mRequestedUri;
+    private Uri mLookupUri;
+    private boolean mLoadGroupMetaData;
+    private boolean mLoadInvitableAccountTypes;
+    private boolean mPostViewNotification;
+    private boolean mComputeFormattedPhoneNumber;
+    private Contact mContact;
+    private ForceLoadContentObserver mObserver;
+    private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
+
+    public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) {
+        this(context, lookupUri, false, false, postViewNotification, false);
+    }
+
+    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
+            boolean loadInvitableAccountTypes,
+            boolean postViewNotification, boolean computeFormattedPhoneNumber) {
+        super(context);
+        mLookupUri = lookupUri;
+        mRequestedUri = lookupUri;
+        mLoadGroupMetaData = loadGroupMetaData;
+        mLoadInvitableAccountTypes = loadInvitableAccountTypes;
+        mPostViewNotification = postViewNotification;
+        mComputeFormattedPhoneNumber = computeFormattedPhoneNumber;
+    }
+
+    /**
+     * Projection used for the query that loads all data for the entire contact (except for
+     * social stream items).
+     */
+    private static class ContactQuery {
+        static final String[] COLUMNS = new String[] {
+                Contacts.NAME_RAW_CONTACT_ID,
+                Contacts.DISPLAY_NAME_SOURCE,
+                Contacts.LOOKUP_KEY,
+                Contacts.DISPLAY_NAME,
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.PHONETIC_NAME,
+                Contacts.PHOTO_ID,
+                Contacts.STARRED,
+                Contacts.CONTACT_PRESENCE,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_TIMESTAMP,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_LABEL,
+                Contacts.Entity.CONTACT_ID,
+                Contacts.Entity.RAW_CONTACT_ID,
+
+                RawContacts.ACCOUNT_NAME,
+                RawContacts.ACCOUNT_TYPE,
+                RawContacts.DATA_SET,
+                RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
+                RawContacts.DIRTY,
+                RawContacts.VERSION,
+                RawContacts.SOURCE_ID,
+                RawContacts.SYNC1,
+                RawContacts.SYNC2,
+                RawContacts.SYNC3,
+                RawContacts.SYNC4,
+                RawContacts.DELETED,
+                RawContacts.NAME_VERIFIED,
+
+                Contacts.Entity.DATA_ID,
+                Data.DATA1,
+                Data.DATA2,
+                Data.DATA3,
+                Data.DATA4,
+                Data.DATA5,
+                Data.DATA6,
+                Data.DATA7,
+                Data.DATA8,
+                Data.DATA9,
+                Data.DATA10,
+                Data.DATA11,
+                Data.DATA12,
+                Data.DATA13,
+                Data.DATA14,
+                Data.DATA15,
+                Data.SYNC1,
+                Data.SYNC2,
+                Data.SYNC3,
+                Data.SYNC4,
+                Data.DATA_VERSION,
+                Data.IS_PRIMARY,
+                Data.IS_SUPER_PRIMARY,
+                Data.MIMETYPE,
+                Data.RES_PACKAGE,
+
+                GroupMembership.GROUP_SOURCE_ID,
+
+                Data.PRESENCE,
+                Data.CHAT_CAPABILITY,
+                Data.STATUS,
+                Data.STATUS_RES_PACKAGE,
+                Data.STATUS_ICON,
+                Data.STATUS_LABEL,
+                Data.STATUS_TIMESTAMP,
+
+                Contacts.PHOTO_URI,
+                Contacts.SEND_TO_VOICEMAIL,
+                Contacts.CUSTOM_RINGTONE,
+                Contacts.IS_USER_PROFILE,
+        };
+
+        public static final int NAME_RAW_CONTACT_ID = 0;
+        public static final int DISPLAY_NAME_SOURCE = 1;
+        public static final int LOOKUP_KEY = 2;
+        public static final int DISPLAY_NAME = 3;
+        public static final int ALT_DISPLAY_NAME = 4;
+        public static final int PHONETIC_NAME = 5;
+        public static final int PHOTO_ID = 6;
+        public static final int STARRED = 7;
+        public static final int CONTACT_PRESENCE = 8;
+        public static final int CONTACT_STATUS = 9;
+        public static final int CONTACT_STATUS_TIMESTAMP = 10;
+        public static final int CONTACT_STATUS_RES_PACKAGE = 11;
+        public static final int CONTACT_STATUS_LABEL = 12;
+        public static final int CONTACT_ID = 13;
+        public static final int RAW_CONTACT_ID = 14;
+
+        public static final int ACCOUNT_NAME = 15;
+        public static final int ACCOUNT_TYPE = 16;
+        public static final int DATA_SET = 17;
+        public static final int ACCOUNT_TYPE_AND_DATA_SET = 18;
+        public static final int DIRTY = 19;
+        public static final int VERSION = 20;
+        public static final int SOURCE_ID = 21;
+        public static final int SYNC1 = 22;
+        public static final int SYNC2 = 23;
+        public static final int SYNC3 = 24;
+        public static final int SYNC4 = 25;
+        public static final int DELETED = 26;
+        public static final int NAME_VERIFIED = 27;
+
+        public static final int DATA_ID = 28;
+        public static final int DATA1 = 29;
+        public static final int DATA2 = 30;
+        public static final int DATA3 = 31;
+        public static final int DATA4 = 32;
+        public static final int DATA5 = 33;
+        public static final int DATA6 = 34;
+        public static final int DATA7 = 35;
+        public static final int DATA8 = 36;
+        public static final int DATA9 = 37;
+        public static final int DATA10 = 38;
+        public static final int DATA11 = 39;
+        public static final int DATA12 = 40;
+        public static final int DATA13 = 41;
+        public static final int DATA14 = 42;
+        public static final int DATA15 = 43;
+        public static final int DATA_SYNC1 = 44;
+        public static final int DATA_SYNC2 = 45;
+        public static final int DATA_SYNC3 = 46;
+        public static final int DATA_SYNC4 = 47;
+        public static final int DATA_VERSION = 48;
+        public static final int IS_PRIMARY = 49;
+        public static final int IS_SUPERPRIMARY = 50;
+        public static final int MIMETYPE = 51;
+        public static final int RES_PACKAGE = 52;
+
+        public static final int GROUP_SOURCE_ID = 53;
+
+        public static final int PRESENCE = 54;
+        public static final int CHAT_CAPABILITY = 55;
+        public static final int STATUS = 56;
+        public static final int STATUS_RES_PACKAGE = 57;
+        public static final int STATUS_ICON = 58;
+        public static final int STATUS_LABEL = 59;
+        public static final int STATUS_TIMESTAMP = 60;
+
+        public static final int PHOTO_URI = 61;
+        public static final int SEND_TO_VOICEMAIL = 62;
+        public static final int CUSTOM_RINGTONE = 63;
+        public static final int IS_USER_PROFILE = 64;
+    }
+
+    /**
+     * Projection used for the query that loads all data for the entire contact.
+     */
+    private static class DirectoryQuery {
+        static final String[] COLUMNS = new String[] {
+            Directory.DISPLAY_NAME,
+            Directory.PACKAGE_NAME,
+            Directory.TYPE_RESOURCE_ID,
+            Directory.ACCOUNT_TYPE,
+            Directory.ACCOUNT_NAME,
+            Directory.EXPORT_SUPPORT,
+        };
+
+        public static final int DISPLAY_NAME = 0;
+        public static final int PACKAGE_NAME = 1;
+        public static final int TYPE_RESOURCE_ID = 2;
+        public static final int ACCOUNT_TYPE = 3;
+        public static final int ACCOUNT_NAME = 4;
+        public static final int EXPORT_SUPPORT = 5;
+    }
+
+    private static class GroupQuery {
+        static final String[] COLUMNS = new String[] {
+            Groups.ACCOUNT_NAME,
+            Groups.ACCOUNT_TYPE,
+            Groups.DATA_SET,
+            Groups.ACCOUNT_TYPE_AND_DATA_SET,
+            Groups._ID,
+            Groups.TITLE,
+            Groups.AUTO_ADD,
+            Groups.FAVORITES,
+        };
+
+        public static final int ACCOUNT_NAME = 0;
+        public static final int ACCOUNT_TYPE = 1;
+        public static final int DATA_SET = 2;
+        public static final int ACCOUNT_TYPE_AND_DATA_SET = 3;
+        public static final int ID = 4;
+        public static final int TITLE = 5;
+        public static final int AUTO_ADD = 6;
+        public static final int FAVORITES = 7;
+    }
+
+    @Override
+    public Contact loadInBackground() {
+        try {
+            final ContentResolver resolver = getContext().getContentResolver();
+            final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(
+                    resolver, mLookupUri);
+            final Contact cachedResult = sCachedResult;
+            sCachedResult = null;
+            // Is this the same Uri as what we had before already? In that case, reuse that 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 Contact(mRequestedUri, cachedResult);
+                resultIsCached = true;
+            } else {
+                if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) {
+                    result = loadEncodedContactEntity(uriCurrentFormat);
+                } else {
+                    result = loadContactEntity(resolver, uriCurrentFormat);
+                }
+                resultIsCached = false;
+            }
+            if (result.isLoaded()) {
+                if (result.isDirectoryEntry()) {
+                    if (!resultIsCached) {
+                        loadDirectoryMetaData(result);
+                    }
+                } else if (mLoadGroupMetaData) {
+                    if (result.getGroupMetaData() == null) {
+                        loadGroupMetaData(result);
+                    }
+                }
+                if (mComputeFormattedPhoneNumber) {
+                    computeFormattedPhoneNumbers(result);
+                }
+                if (!resultIsCached) loadPhotoBinaryData(result);
+
+                // Note ME profile should never have "Add connection"
+                if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
+                    loadInvitableAccountTypes(result);
+                }
+            }
+            return result;
+        } catch (Exception e) {
+            Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
+            return Contact.forError(mRequestedUri, e);
+        }
+    }
+
+    private Contact loadEncodedContactEntity(Uri uri) throws JSONException {
+        final String jsonString = uri.getEncodedFragment();
+        final JSONObject json = new JSONObject(jsonString);
+
+        final long directoryId =
+                Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY));
+
+        final String displayName = json.getString(Contacts.DISPLAY_NAME);
+        final String altDisplayName = json.optString(
+                Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+        final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE);
+        final String photoUri = json.optString(Contacts.PHOTO_URI, null);
+        final Contact contact = new Contact(
+                uri, uri,
+                mLookupUri,
+                directoryId,
+                null /* lookupKey */,
+                -1 /* id */,
+                -1 /* nameRawContactId */,
+                displayNameSource,
+                0 /* photoId */,
+                photoUri,
+                displayName,
+                altDisplayName,
+                null /* phoneticName */,
+                false /* starred */,
+                null /* presence */,
+                false /* sendToVoicemail */,
+                null /* customRingtone */,
+                false /* isUserProfile */);
+
+        contact.setStatuses(new ImmutableMap.Builder<Long, DataStatus>().build());
+
+        final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null);
+        final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME);
+        if (accountName != null) {
+            final String accountType = json.getString(RawContacts.ACCOUNT_TYPE);
+            contact.setDirectoryMetaData(directoryName, null, accountName, accountType,
+                    json.optInt(Directory.EXPORT_SUPPORT,
+                            Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY));
+        } else {
+            contact.setDirectoryMetaData(directoryName, null, null, null,
+                    json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT));
+        }
+
+        final ContentValues values = new ContentValues();
+        values.put(Data._ID, -1);
+        values.put(Data.CONTACT_ID, -1);
+        final RawContact rawContact = new RawContact(values);
+
+        final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+        final Iterator keys = items.keys();
+        while (keys.hasNext()) {
+            final String mimetype = (String) keys.next();
+
+            // Could be single object or array.
+            final JSONObject obj = items.optJSONObject(mimetype);
+            if (obj == null) {
+                final JSONArray array = items.getJSONArray(mimetype);
+                for (int i = 0; i < array.length(); i++) {
+                    final JSONObject item = array.getJSONObject(i);
+                    processOneRecord(rawContact, item, mimetype);
+                }
+            } else {
+                processOneRecord(rawContact, obj, mimetype);
+            }
+        }
+
+        contact.setRawContacts(new ImmutableList.Builder<RawContact>()
+                .add(rawContact)
+                .build());
+        return contact;
+    }
+
+    private void processOneRecord(RawContact rawContact, JSONObject item, String mimetype)
+            throws JSONException {
+        final ContentValues itemValues = new ContentValues();
+        itemValues.put(Data.MIMETYPE, mimetype);
+        itemValues.put(Data._ID, -1);
+
+        final Iterator iterator = item.keys();
+        while (iterator.hasNext()) {
+            String name = (String) iterator.next();
+            final Object o = item.get(name);
+            if (o instanceof String) {
+                itemValues.put(name, (String) o);
+            } else if (o instanceof Integer) {
+                itemValues.put(name, (Integer) o);
+            }
+        }
+        rawContact.addDataItemValues(itemValues);
+    }
+
+    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 Contact.forNotFound(mRequestedUri);
+        }
+
+        try {
+            if (!cursor.moveToFirst()) {
+                cursor.close();
+                return Contact.forNotFound(mRequestedUri);
+            }
+
+            // 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;
+            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;
+                    rawContact = new RawContact(loadRawContactValues(cursor));
+                    rawContactsBuilder.add(rawContact);
+                }
+                if (!cursor.isNull(ContactQuery.DATA_ID)) {
+                    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);
+                        statusesBuilder.put(dataId, status);
+                    }
+                }
+            } while (cursor.moveToNext());
+
+            contact.setRawContacts(rawContactsBuilder.build());
+            contact.setStatuses(statusesBuilder.build());
+
+            return contact;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
+     * not found, returns null
+     */
+    private void loadPhotoBinaryData(Contact contactData) {
+        // If we have a photo URI, try loading that first.
+        String photoUri = contactData.getPhotoUri();
+        if (photoUri != null) {
+            try {
+                final InputStream inputStream;
+                final AssetFileDescriptor fd;
+                final Uri uri = Uri.parse(photoUri);
+                final String scheme = uri.getScheme();
+                if ("http".equals(scheme) || "https".equals(scheme)) {
+                    // Support HTTP urls that might come from extended directories
+                    inputStream = new URL(photoUri).openStream();
+                    fd = null;
+                } else {
+                    fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r");
+                    inputStream = fd.createInputStream();
+                }
+                byte[] buffer = new byte[16 * 1024];
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                try {
+                    int size;
+                    while ((size = inputStream.read(buffer)) != -1) {
+                        baos.write(buffer, 0, size);
+                    }
+                    contactData.setPhotoBinaryData(baos.toByteArray());
+                } finally {
+                    inputStream.close();
+                    if (fd != null) {
+                        fd.close();
+                    }
+                }
+                return;
+            } catch (IOException ioe) {
+                // Just fall back to the case below.
+            }
+        }
+
+        // If we couldn't load from a file, fall back to the data blob.
+        final long photoId = contactData.getPhotoId();
+        if (photoId <= 0) {
+            // No photo ID
+            return;
+        }
+
+        for (RawContact rawContact : contactData.getRawContacts()) {
+            for (DataItem dataItem : rawContact.getDataItems()) {
+                if (dataItem.getId() == photoId) {
+                    if (!(dataItem instanceof PhotoDataItem)) {
+                        break;
+                    }
+
+                    final PhotoDataItem photo = (PhotoDataItem) dataItem;
+                    contactData.setPhotoBinaryData(photo.getPhoto());
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}.
+     */
+    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();
+            if (!invitables.isEmpty()) {
+                final Map<AccountTypeWithDataSet, AccountType> resultMap =
+                        Maps.newHashMap(invitables);
+
+                // Remove the ones that already have a raw contact in the current contact
+                for (RawContact rawContact : contactData.getRawContacts()) {
+                    final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
+                            rawContact.getAccountTypeString(),
+                            rawContact.getDataSet());
+                    resultMap.remove(type);
+                }
+
+                resultListBuilder.addAll(resultMap.values());
+            }
+        }
+
+        // Set to mInvitableAccountTypes
+        contactData.setInvitableAccountTypes(resultListBuilder.build());
+    }
+
+    /**
+     * Extracts Contact level columns from the cursor.
+     */
+    private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
+        final String directoryParameter =
+                contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+        final long directoryId = directoryParameter == null
+                ? Directory.DEFAULT
+                : Long.parseLong(directoryParameter);
+        final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+        final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
+        final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+        final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+        final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+        final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
+        final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+        final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+        final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
+        final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+        final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+                ? null
+                : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+        final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
+        final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
+        final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
+
+        Uri lookupUri;
+        if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
+            lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
+        } else {
+            lookupUri = contactUri;
+        }
+
+        return new Contact(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
+                contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
+                altDisplayName, phoneticName, starred, presence, sendToVoicemail,
+                customRingtone, isUserProfile);
+    }
+
+    /**
+     * Extracts RawContact level columns from the cursor.
+     */
+    private ContentValues loadRawContactValues(Cursor cursor) {
+        ContentValues cv = new ContentValues();
+
+        cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
+
+        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED);
+
+        return cv;
+    }
+
+    /**
+     * Extracts Data level columns from the cursor.
+     */
+    private ContentValues loadDataValues(Cursor cursor) {
+        ContentValues cv = new ContentValues();
+
+        cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
+
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
+
+        return cv;
+    }
+
+    private void cursorColumnToContentValues(
+            Cursor cursor, ContentValues values, int index) {
+        switch (cursor.getType(index)) {
+            case Cursor.FIELD_TYPE_NULL:
+                // don't put anything in the content values
+                break;
+            case Cursor.FIELD_TYPE_INTEGER:
+                values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
+                break;
+            case Cursor.FIELD_TYPE_STRING:
+                values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
+                break;
+            case Cursor.FIELD_TYPE_BLOB:
+                values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
+                break;
+            default:
+                throw new IllegalStateException("Invalid or unhandled data type");
+        }
+    }
+
+    private void loadDirectoryMetaData(Contact result) {
+        long directoryId = result.getDirectoryId();
+
+        Cursor cursor = getContext().getContentResolver().query(
+                ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
+                DirectoryQuery.COLUMNS, null, null, null);
+        if (cursor == null) {
+            return;
+        }
+        try {
+            if (cursor.moveToFirst()) {
+                final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+                final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+                final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+                final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+                final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+                final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
+                String directoryType = null;
+                if (!TextUtils.isEmpty(packageName)) {
+                    PackageManager pm = getContext().getPackageManager();
+                    try {
+                        Resources resources = pm.getResourcesForApplication(packageName);
+                        directoryType = resources.getString(typeResourceId);
+                    } catch (NameNotFoundException e) {
+                        Log.w(TAG, "Contact directory resource not found: "
+                                + packageName + "." + typeResourceId);
+                    }
+                }
+
+                result.setDirectoryMetaData(
+                        displayName, directoryType, accountType, accountName, exportSupport);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Loads groups meta-data for all groups associated with all constituent raw contacts'
+     * accounts.
+     */
+    private void loadGroupMetaData(Contact result) {
+        StringBuilder selection = new StringBuilder();
+        ArrayList<String> selectionArgs = new ArrayList<String>();
+        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 ");
+                }
+                selection.append(
+                        "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
+                selectionArgs.add(accountName);
+                selectionArgs.add(accountType);
+
+                if (dataSet != null) {
+                    selection.append(" AND " + Groups.DATA_SET + "=?");
+                    selectionArgs.add(dataSet);
+                } else {
+                    selection.append(" AND " + Groups.DATA_SET + " IS NULL");
+                }
+                selection.append(")");
+            }
+        }
+        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);
+        try {
+            while (cursor.moveToNext()) {
+                final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+                final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+                final String dataSet = cursor.getString(GroupQuery.DATA_SET);
+                final long groupId = cursor.getLong(GroupQuery.ID);
+                final String title = cursor.getString(GroupQuery.TITLE);
+                final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
+                        ? false
+                        : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
+                final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
+                        ? false
+                        : cursor.getInt(GroupQuery.FAVORITES) != 0;
+
+                groupListBuilder.add(new GroupMetaData(
+                        accountName, accountType, dataSet, groupId, title, defaultGroup,
+                        favorites));
+            }
+        } finally {
+            cursor.close();
+        }
+        result.setGroupMetaData(groupListBuilder.build());
+    }
+
+    /**
+     * Iterates over all data items that represent phone numbers are tries to calculate a formatted
+     * number. This function can safely be called several times as no unformatted data is
+     * overwritten
+     */
+    private void computeFormattedPhoneNumbers(Contact contactData) {
+        final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+        final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
+        final int rawContactCount = rawContacts.size();
+        for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
+            final RawContact rawContact = rawContacts.get(rawContactIndex);
+            final List<DataItem> dataItems = rawContact.getDataItems();
+            final int dataCount = dataItems.size();
+            for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
+                final DataItem dataItem = dataItems.get(dataIndex);
+                if (dataItem instanceof PhoneDataItem) {
+                    final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
+                    phoneDataItem.computeFormattedPhoneNumber(countryIso);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void deliverResult(Contact result) {
+        unregisterObserver();
+
+        // The creator isn't interested in any further updates
+        if (isReset() || result == null) {
+            return;
+        }
+
+        mContact = result;
+
+        if (result.isLoaded()) {
+            mLookupUri = result.getLookupUri();
+
+            if (!result.isDirectoryEntry()) {
+                Log.i(TAG, "Registering content observer for " + mLookupUri);
+                if (mObserver == null) {
+                    mObserver = new ForceLoadContentObserver();
+                }
+                getContext().getContentResolver().registerContentObserver(
+                        mLookupUri, true, mObserver);
+            }
+
+            if (mPostViewNotification) {
+                // inform the source of the data that this contact is being looked at
+                postViewNotificationToSyncAdapter();
+            }
+        }
+
+        super.deliverResult(mContact);
+    }
+
+    /**
+     * Posts a message to the contributing sync adapters that have opted-in, notifying them
+     * that the contact has just been loaded
+     */
+    private void postViewNotificationToSyncAdapter() {
+        Context context = getContext();
+        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 AccountType accountType = rawContact.getAccountType(context);
+            final String serviceName = accountType.getViewContactNotifyServiceClassName();
+            final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
+            if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
+                final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+                final Intent intent = new Intent();
+                intent.setClassName(servicePackageName, serviceName);
+                intent.setAction(Intent.ACTION_VIEW);
+                intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
+                try {
+                    context.startService(intent);
+                } catch (Exception e) {
+                    Log.e(TAG, "Error sending message to source-app", e);
+                }
+            }
+        }
+    }
+
+    private void unregisterObserver() {
+        if (mObserver != null) {
+            getContext().getContentResolver().unregisterContentObserver(mObserver);
+            mObserver = null;
+        }
+    }
+
+    /**
+     * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the
+     * new result will be delivered
+     */
+    public void upgradeToFullContact() {
+        // Everything requested already? Nothing to do, so let's bail out
+        if (mLoadGroupMetaData && mLoadInvitableAccountTypes
+                && mPostViewNotification && mComputeFormattedPhoneNumber) return;
+
+        mLoadGroupMetaData = true;
+        mLoadInvitableAccountTypes = true;
+        mPostViewNotification = true;
+        mComputeFormattedPhoneNumber = true;
+
+        // Cache the current result, so that we only load the "missing" parts of the contact.
+        cacheResult();
+
+        // Our load parameters have changed, so let's pretend the data has changed. Its the same
+        // thing, essentially.
+        onContentChanged();
+    }
+
+    public Uri getLookupUri() {
+        return mLookupUri;
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mContact != null) {
+            deliverResult(mContact);
+        }
+
+        if (takeContentChanged() || mContact == null) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        cancelLoad();
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+        cancelLoad();
+        unregisterObserver();
+        mContact = null;
+    }
+
+    /**
+     * Caches the result, which is useful when we switch from activity to activity, using the same
+     * contact. If the next load is for a different contact, the cached result will be dropped
+     */
+    public void cacheResult() {
+        if (mContact == null || !mContact.isLoaded()) {
+            sCachedResult = null;
+        } else {
+            sCachedResult = mContact;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/RawContact.java b/src/com/android/contacts/common/model/RawContact.java
new file mode 100644
index 0000000..e5fd06a
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContact.java
@@ -0,0 +1,374 @@
+/*
+ * 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.common.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+
+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.).
+ */
+final public class RawContact implements Parcelable {
+
+    private AccountTypeManager mAccountTypeManager;
+    private final ContentValues mValues;
+    private final ArrayList<NamedDataItem> mDataItems;
+
+    final public static class NamedDataItem implements Parcelable {
+        public final Uri mUri;
+
+        // This use to be a DataItem. DataItem creation is now delayed until the point of request
+        // since there is no benefit to storing them here due to the multiple inheritance.
+        // Eventually instanceof still has to be used anyways to determine which sub-class of
+        // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or
+        // parcelable.
+        //
+        // Instead of having a common DataItem super class, we should refactor this to be a generic
+        // Object where the object is a concrete class that no longer relies on ContentValues.
+        // (this will also make the classes easier to use).
+        // Since instanceof is used later anyways, having a list of Objects won't hurt and is no
+        // worse than having a DataItem.
+        public final ContentValues mContentValues;
+
+        public NamedDataItem(Uri uri, ContentValues values) {
+            this.mUri = uri;
+            this.mContentValues = values;
+        }
+
+        public NamedDataItem(Parcel parcel) {
+            this.mUri = parcel.readParcelable(Uri.class.getClassLoader());
+            this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel parcel, int i) {
+            parcel.writeParcelable(mUri, i);
+            parcel.writeParcelable(mContentValues, i);
+        }
+
+        public static final Parcelable.Creator<NamedDataItem> CREATOR
+                = new Parcelable.Creator<NamedDataItem>() {
+
+            @Override
+            public NamedDataItem createFromParcel(Parcel parcel) {
+                return new NamedDataItem(parcel);
+            }
+
+            @Override
+            public NamedDataItem[] newArray(int i) {
+                return new NamedDataItem[i];
+            }
+        };
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(mUri, mContentValues);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+
+            final NamedDataItem other = (NamedDataItem) obj;
+            return Objects.equal(mUri, other.mUri) &&
+                    Objects.equal(mContentValues, other.mContentValues);
+        }
+    }
+
+    public static RawContact createFrom(Entity entity) {
+        final ContentValues values = entity.getEntityValues();
+        final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
+
+        RawContact rawContact = new RawContact(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.
+     */
+    public RawContact() {
+        this(new ContentValues());
+    }
+
+    public RawContact(ContentValues values) {
+        mValues = values;
+        mDataItems = new ArrayList<NamedDataItem>();
+    }
+
+    /**
+     * Constructor for the parcelable.
+     *
+     * @param parcel The parcel to de-serialize from.
+     */
+    private RawContact(Parcel parcel) {
+        mValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+        mDataItems = Lists.newArrayList();
+        parcel.readTypedList(mDataItems, NamedDataItem.CREATOR);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int i) {
+        parcel.writeParcelable(mValues, i);
+        parcel.writeTypedList(mDataItems);
+    }
+
+    /**
+     * Create for building the parcelable.
+     */
+    public static final Parcelable.Creator<RawContact> CREATOR
+            = new Parcelable.Creator<RawContact>() {
+
+        @Override
+        public RawContact createFromParcel(Parcel parcel) {
+            return new RawContact(parcel);
+        }
+
+        @Override
+        public RawContact[] newArray(int i) {
+            return new RawContact[i];
+        }
+    };
+
+    public AccountTypeManager getAccountTypeManager(Context context) {
+        if (mAccountTypeManager == null) {
+            mAccountTypeManager = AccountTypeManager.getInstance(context);
+        }
+        return mAccountTypeManager;
+    }
+
+    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 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(Context context) {
+        return getAccountTypeManager(context).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);
+                }
+                return;
+            }
+        }
+        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);
+    }
+
+    /**
+     * Creates and inserts a DataItem object that wraps the content values, and returns it.
+     */
+    public void addDataItemValues(ContentValues values) {
+        addNamedDataItemValues(Data.CONTENT_URI, values);
+    }
+
+    public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) {
+        final NamedDataItem namedItem = new NamedDataItem(uri, values);
+        mDataItems.add(namedItem);
+        return namedItem;
+    }
+
+    public ArrayList<ContentValues> getContentValues() {
+        final ArrayList<ContentValues> list = Lists.newArrayListWithCapacity(mDataItems.size());
+        for (NamedDataItem dataItem : mDataItems) {
+            if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+                list.add(dataItem.mContentValues);
+            }
+        }
+        return list;
+    }
+
+    public List<DataItem> getDataItems() {
+        final ArrayList<DataItem> list = Lists.newArrayListWithCapacity(mDataItems.size());
+        for (NamedDataItem dataItem : mDataItems) {
+            if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+                list.add(DataItem.createFrom(dataItem.mContentValues));
+            }
+        }
+        return list;
+    }
+
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("RawContact: ").append(mValues);
+        for (RawContact.NamedDataItem namedDataItem : mDataItems) {
+            sb.append("\n  ").append(namedDataItem.mUri);
+            sb.append("\n  -> ").append(namedDataItem.mContentValues);
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mValues, mDataItems);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+
+        RawContact other = (RawContact) obj;
+        return Objects.equal(mValues, other.mValues) &&
+                Objects.equal(mDataItems, other.mDataItems);
+    }
+}
diff --git a/src/com/android/contacts/common/model/RawContactDelta.java b/src/com/android/contacts/common/model/RawContactDelta.java
new file mode 100644
index 0000000..7a20041
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactDelta.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Profile;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.test.NeededForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * 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 RawContact}, which then becomes an insert {@link RawContacts} case.
+ * <p>
+ * 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 RawContact}, we know the original data must
+ * be deleted, but to preserve the user modifications we treat as an insert.
+ */
+public class RawContactDelta implements Parcelable {
+    // TODO: optimize by using contentvalues pool, since we allocate so many of them
+
+    private static final String TAG = "EntityDelta";
+    private static final boolean LOGV = false;
+
+    /**
+     * Direct values from {@link Entity#getEntityValues()}.
+     */
+    private ValuesDelta mValues;
+
+    /**
+     * URI used for contacts queries, by default it is set to query raw contacts.
+     * It can be set to query the profile's raw contact(s).
+     */
+    private Uri mContactsQueryUri = RawContacts.CONTENT_URI;
+
+    /**
+     * Internal map of children values from {@link Entity#getSubValues()}, which
+     * we store here sorted into {@link Data#MIMETYPE} bins.
+     */
+    private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
+
+    public RawContactDelta() {
+    }
+
+    public RawContactDelta(ValuesDelta values) {
+        mValues = values;
+    }
+
+    /**
+     * Build an {@link RawContactDelta} using the given {@link RawContact} as a
+     * starting point; the "before" snapshot.
+     */
+    public static RawContactDelta fromBefore(RawContact before) {
+        final RawContactDelta rawContactDelta = new RawContactDelta();
+        rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
+        rawContactDelta.mValues.setIdColumn(RawContacts._ID);
+        for (final ContentValues values : before.getContentValues()) {
+            rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
+        }
+        return rawContactDelta;
+    }
+
+    /**
+     * 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 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 RawContactDelta();
+
+        if (LOGV) {
+            final Long localVersion = (local.mValues == null) ? null : local.mValues
+                    .getAsLong(RawContacts.VERSION);
+            final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
+            Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
+                    + localVersion);
+        }
+
+        // Create values if needed, and merge "after" changes
+        local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
+
+        // Find matching local entry for each remote values, or create
+        for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
+            for (ValuesDelta remoteEntry : mimeEntries) {
+                final Long childId = remoteEntry.getId();
+
+                // Find or create local match and merge
+                final ValuesDelta localEntry = local.getEntry(childId);
+                final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
+
+                if (localEntry == null && merged != null) {
+                    // No local entry before, so insert
+                    local.addEntry(merged);
+                }
+            }
+        }
+
+        return local;
+    }
+
+    public ValuesDelta getValues() {
+        return mValues;
+    }
+
+    public boolean isContactInsert() {
+        return mValues.isInsert();
+    }
+
+    /**
+     * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
+     * which may return null when no entry exists.
+     */
+    public ValuesDelta getPrimaryEntry(String mimeType) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+        if (mimeEntries == null) return null;
+
+        for (ValuesDelta entry : mimeEntries) {
+            if (entry.isPrimary()) {
+                return entry;
+            }
+        }
+
+        // When no direct primary, return something
+        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
+    }
+
+    /**
+     * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
+     * @see #getSuperPrimaryEntry(String, boolean)
+     */
+    public ValuesDelta getSuperPrimaryEntry(String mimeType) {
+        return getSuperPrimaryEntry(mimeType, true);
+    }
+
+    /**
+     * Returns the super-primary entry for the given mime type
+     * @param forceSelection if true, will try to return some value even if a super-primary
+     *     doesn't exist (may be a primary, or just a random item
+     * @return
+     */
+    @NeededForTesting
+    public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+        if (mimeEntries == null) return null;
+
+        ValuesDelta primary = null;
+        for (ValuesDelta entry : mimeEntries) {
+            if (entry.isSuperPrimary()) {
+                return entry;
+            } else if (entry.isPrimary()) {
+                primary = entry;
+            }
+        }
+
+        if (!forceSelection) {
+            return null;
+        }
+
+        // When no direct super primary, return something
+        if (primary != null) {
+            return primary;
+        }
+        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
+    }
+
+    /**
+     * Return the AccountType that this raw-contact belongs to.
+     */
+    public AccountType getRawContactAccountType(Context context) {
+        ContentValues entityValues = getValues().getCompleteValues();
+        String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
+        String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
+        return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
+    }
+
+    public Long getRawContactId() {
+        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.
+     */
+    private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
+        ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
+        if (mimeEntries == null && lazyCreate) {
+            mimeEntries = Lists.newArrayList();
+            mEntries.put(mimeType, mimeEntries);
+        }
+        return mimeEntries;
+    }
+
+    public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
+        return getMimeEntries(mimeType, false);
+    }
+
+    public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
+        if (mimeEntries == null) return 0;
+
+        int count = 0;
+        for (ValuesDelta child : mimeEntries) {
+            // Skip deleted items when requesting only visible
+            if (onlyVisible && !child.isVisible()) continue;
+            count++;
+        }
+        return count;
+    }
+
+    public boolean hasMimeEntries(String mimeType) {
+        return mEntries.containsKey(mimeType);
+    }
+
+    public ValuesDelta addEntry(ValuesDelta entry) {
+        final String mimeType = entry.getMimetype();
+        getMimeEntries(mimeType, true).add(entry);
+        return entry;
+    }
+
+    public ArrayList<ContentValues> getContentValues() {
+        ArrayList<ContentValues> values = Lists.newArrayList();
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta entry : mimeEntries) {
+                if (!entry.isDelete()) {
+                    values.add(entry.getCompleteValues());
+                }
+            }
+        }
+        return values;
+    }
+
+    /**
+     * Find entry with the given {@link BaseColumns#_ID} value.
+     */
+    public ValuesDelta getEntry(Long childId) {
+        if (childId == null) {
+            // Requesting an "insert" entry, which has no "before"
+            return null;
+        }
+
+        // Search all children for requested entry
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta entry : mimeEntries) {
+                if (childId.equals(entry.getId())) {
+                    return entry;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the total number of {@link ValuesDelta} contained.
+     */
+    public int getEntryCount(boolean onlyVisible) {
+        int count = 0;
+        for (String mimeType : mEntries.keySet()) {
+            count += getMimeEntriesCount(mimeType, onlyVisible);
+        }
+        return count;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (object instanceof RawContactDelta) {
+            final RawContactDelta other = (RawContactDelta)object;
+
+            // Equality failed if parent values different
+            if (!other.mValues.equals(mValues)) return false;
+
+            for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+                for (ValuesDelta child : mimeEntries) {
+                    // Equality failed if any children unmatched
+                    if (!other.containsEntry(child)) return false;
+                }
+            }
+
+            // Passed all tests, so equal
+            return true;
+        }
+        return false;
+    }
+
+    private boolean containsEntry(ValuesDelta entry) {
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                // Contained if we find any child that matches
+                if (child.equals(entry)) return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Mark this entire object deleted, including any {@link ValuesDelta}.
+     */
+    public void markDeleted() {
+        this.mValues.markDeleted();
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                child.markDeleted();
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("\n(");
+        builder.append("Uri=");
+        builder.append(mContactsQueryUri);
+        builder.append(", Values=");
+        builder.append(mValues != null ? mValues.toString() : "null");
+        builder.append(", Entries={");
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                builder.append("\n\t");
+                child.toString(builder);
+            }
+        }
+        builder.append("\n})\n");
+        return builder.toString();
+    }
+
+    /**
+     * Consider building the given {@link ContentProviderOperation.Builder} and
+     * appending it to the given list, which only happens if builder is valid.
+     */
+    private void possibleAdd(ArrayList<ContentProviderOperation> diff,
+            ContentProviderOperation.Builder builder) {
+        if (builder != null) {
+            diff.add(builder.build());
+        }
+    }
+
+    /**
+     * Build a list of {@link ContentProviderOperation} that will assert any
+     * "before" state hasn't changed. This is maintained separately so that all
+     * asserts can take place before any updates occur.
+     */
+    public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
+        final boolean isContactInsert = mValues.isInsert();
+        if (!isContactInsert) {
+            // Assert version is consistent while persisting changes
+            final Long beforeId = mValues.getId();
+            final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
+            if (beforeId == null || beforeVersion == null) return;
+
+            final ContentProviderOperation.Builder builder = ContentProviderOperation
+                    .newAssertQuery(mContactsQueryUri);
+            builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+            builder.withValue(RawContacts.VERSION, beforeVersion);
+            buildInto.add(builder.build());
+        }
+    }
+
+    /**
+     * Build a list of {@link ContentProviderOperation} that will transform the
+     * current "before" {@link Entity} state into the modified state which this
+     * {@link RawContactDelta} represents.
+     */
+    public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
+        final int firstIndex = buildInto.size();
+
+        final boolean isContactInsert = mValues.isInsert();
+        final boolean isContactDelete = mValues.isDelete();
+        final boolean isContactUpdate = !isContactInsert && !isContactDelete;
+
+        final Long beforeId = mValues.getId();
+
+        Builder builder;
+
+        if (isContactInsert) {
+            // TODO: for now simply disabling aggregation when a new contact is
+            // created on the phone.  In the future, will show aggregation suggestions
+            // after saving the contact.
+            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+        }
+
+        // Build possible operation at Contact level
+        builder = mValues.buildDiff(mContactsQueryUri);
+        possibleAdd(buildInto, builder);
+
+        // Build operations for all children
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                // Ignore children if parent was deleted
+                if (isContactDelete) continue;
+
+                // Use the profile data URI if the contact is the profile.
+                if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
+                    builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI,
+                            RawContacts.Data.CONTENT_DIRECTORY));
+                } else {
+                    builder = child.buildDiff(Data.CONTENT_URI);
+                }
+
+                if (child.isInsert()) {
+                    if (isContactInsert) {
+                        // Parent is brand new insert, so back-reference _id
+                        builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
+                    } else {
+                        // Inserting under existing, so fill with known _id
+                        builder.withValue(Data.RAW_CONTACT_ID, beforeId);
+                    }
+                } else if (isContactInsert && builder != null) {
+                    // Child must be insert when Contact insert
+                    throw new IllegalArgumentException("When parent insert, child must be also");
+                }
+                possibleAdd(buildInto, builder);
+            }
+        }
+
+        final boolean addedOperations = buildInto.size() > firstIndex;
+        if (addedOperations && isContactUpdate) {
+            // Suspend aggregation while persisting updates
+            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
+            buildInto.add(firstIndex, builder.build());
+
+            // Restore aggregation mode as last operation
+            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
+            buildInto.add(builder.build());
+        } else if (isContactInsert) {
+            // Restore aggregation mode as last operation
+            builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
+            builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+            builder.withSelection(RawContacts._ID + "=?", new String[1]);
+            builder.withSelectionBackReference(0, firstIndex);
+            buildInto.add(builder.build());
+        }
+    }
+
+    /**
+     * Build a {@link ContentProviderOperation} that changes
+     * {@link RawContacts#AGGREGATION_MODE} to the given value.
+     */
+    protected Builder buildSetAggregationMode(Long beforeId, int mode) {
+        Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
+        builder.withValue(RawContacts.AGGREGATION_MODE, mode);
+        builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+        return builder;
+    }
+
+    /** {@inheritDoc} */
+    public int describeContents() {
+        // Nothing special about this parcel
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    public void writeToParcel(Parcel dest, int flags) {
+        final int size = this.getEntryCount(false);
+        dest.writeInt(size);
+        dest.writeParcelable(mValues, flags);
+        dest.writeParcelable(mContactsQueryUri, flags);
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                dest.writeParcelable(child, flags);
+            }
+        }
+    }
+
+    public void readFromParcel(Parcel source) {
+        final ClassLoader loader = getClass().getClassLoader();
+        final int size = source.readInt();
+        mValues = source.<ValuesDelta> readParcelable(loader);
+        mContactsQueryUri = source.<Uri> readParcelable(loader);
+        for (int i = 0; i < size; i++) {
+            final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
+            this.addEntry(child);
+        }
+    }
+
+    /**
+     * Used to set the query URI to the profile URI to store profiles.
+     */
+    public void setProfileQueryUri() {
+        mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI;
+    }
+
+    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 RawContactDelta[] newArray(int size) {
+            return new RawContactDelta[size];
+        }
+    };
+
+}
diff --git a/src/com/android/contacts/common/model/RawContactDeltaList.java b/src/com/android/contacts/common/model/RawContactDeltaList.java
new file mode 100644
index 0000000..f3070c4
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactDeltaList.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.common.model.ValuesDelta;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * Container for multiple {@link RawContactDelta} objects, usually when editing
+ * together as an entire aggregate. Provides convenience methods for parceling
+ * and applying another {@link RawContactDeltaList} over it.
+ */
+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;
+
+    public RawContactDeltaList() {
+    }
+
+    /**
+     * 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 RawContactDeltaList fromQuery(Uri entityUri, ContentResolver resolver,
+            String selection, String[] selectionArgs, String sortOrder) {
+        final EntityIterator iterator = RawContacts.newEntityIterator(
+                resolver.query(entityUri, null, selection, selectionArgs, sortOrder));
+        try {
+            return fromIterator(iterator);
+        } finally {
+            iterator.close();
+        }
+    }
+
+    /**
+     * 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 RawContactDeltaList fromIterator(Iterator<?> iterator) {
+        final RawContactDeltaList state = new RawContactDeltaList();
+        state.addAll(iterator);
+        return state;
+    }
+
+    public void addAll(Iterator<?> iterator) {
+        // Perform background query to pull contact details
+        while (iterator.hasNext()) {
+            // Read all contacts into local deltas to prepare for edits
+            Object nextObject = iterator.next();
+            final RawContact before = nextObject instanceof Entity
+                    ? RawContact.createFrom((Entity) nextObject)
+                    : (RawContact) nextObject;
+            final RawContactDelta rawContactDelta = RawContactDelta.fromBefore(before);
+            add(rawContactDelta);
+        }
+    }
+
+    /**
+     * 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 RawContactDeltaList}.
+     */
+    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 (RawContactDelta remoteEntity : remote) {
+            final Long rawContactId = remoteEntity.getValues().getId();
+
+            // Find or create local match and merge
+            final RawContactDelta localEntity = local.getByRawContactId(rawContactId);
+            final RawContactDelta merged = RawContactDelta.mergeAfter(localEntity, remoteEntity);
+
+            if (localEntity == null && merged != null) {
+                // No local entry before, so insert
+                local.add(merged);
+            }
+        }
+
+        return local;
+    }
+
+    /**
+     * Build a list of {@link ContentProviderOperation} that will transform all
+     * the "before" {@link Entity} states into the modified state which all
+     * {@link RawContactDelta} objects represent. This method specifically creates
+     * any {@link AggregationExceptions} rules needed to groups edits together.
+     */
+    public ArrayList<ContentProviderOperation> buildDiff() {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "buildDiff: list=" + toString());
+        }
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+
+        final long rawContactId = this.findRawContactId();
+        int firstInsertRow = -1;
+
+        // First pass enforces versions remain consistent
+        for (RawContactDelta delta : this) {
+            delta.buildAssert(diff);
+        }
+
+        final int assertMark = diff.size();
+        int backRefs[] = new int[size()];
+
+        int rawContactIndex = 0;
+
+        // Second pass builds actual operations
+        for (RawContactDelta delta : this) {
+            final int firstBatch = diff.size();
+            final boolean isInsert = delta.isContactInsert();
+            backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
+
+            delta.buildDiff(diff);
+
+            // If the user chose to join with some other existing raw contact(s) at save time,
+            // add aggregation exceptions for all those raw contacts.
+            if (mJoinWithRawContactIds != null) {
+                for (Long joinedRawContactId : mJoinWithRawContactIds) {
+                    final Builder builder = beginKeepTogether();
+                    builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId);
+                    if (rawContactId != -1) {
+                        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
+                    } else {
+                        builder.withValueBackReference(
+                                AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                    }
+                    diff.add(builder.build());
+                }
+            }
+
+            // Only create rules for inserts
+            if (!isInsert) continue;
+
+            // If we are going to split all contacts, there is no point in first combining them
+            if (mSplitRawContacts) continue;
+
+            if (rawContactId != -1) {
+                // Has existing contact, so bind to it strongly
+                final Builder builder = beginKeepTogether();
+                builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                diff.add(builder.build());
+
+            } else if (firstInsertRow == -1) {
+                // First insert case, so record row
+                firstInsertRow = firstBatch;
+
+            } else {
+                // Additional insert case, so point at first insert
+                final Builder builder = beginKeepTogether();
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1,
+                        firstInsertRow);
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                diff.add(builder.build());
+            }
+        }
+
+        if (mSplitRawContacts) {
+            buildSplitContactDiff(diff, backRefs);
+        }
+
+        // No real changes if only left with asserts
+        if (diff.size() == assertMark) {
+            diff.clear();
+        }
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "buildDiff: ops=" + diffToString(diff));
+        }
+        return diff;
+    }
+
+    private static String diffToString(ArrayList<ContentProviderOperation> ops) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[\n");
+        for (ContentProviderOperation op : ops) {
+            sb.append(op.toString());
+            sb.append(",\n");
+        }
+        sb.append("]\n");
+        return sb.toString();
+    }
+
+    /**
+     * Start building a {@link ContentProviderOperation} that will keep two
+     * {@link RawContacts} together.
+     */
+    protected Builder beginKeepTogether() {
+        final Builder builder = ContentProviderOperation
+                .newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+        return builder;
+    }
+
+    /**
+     * Builds {@link AggregationExceptions} to split all constituent raw contacts into
+     * separate contacts.
+     */
+    private void buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff,
+            int[] backRefs) {
+        int count = size();
+        for (int i = 0; i < count; i++) {
+            for (int j = 0; j < count; j++) {
+                if (i != j) {
+                    buildSplitContactDiff(diff, i, j, backRefs);
+                }
+            }
+        }
+    }
+
+    /**
+     * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
+     */
+    private void buildSplitContactDiff(ArrayList<ContentProviderOperation> diff, int index1,
+            int index2, int[] backRefs) {
+        Builder builder =
+                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
+
+        Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID);
+        int backRef1 = backRefs[index1];
+        if (rawContactId1 != null && rawContactId1 >= 0) {
+            builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+        } else if (backRef1 >= 0) {
+            builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRef1);
+        } else {
+            return;
+        }
+
+        Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID);
+        int backRef2 = backRefs[index2];
+        if (rawContactId2 != null && rawContactId2 >= 0) {
+            builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+        } else if (backRef2 >= 0) {
+            builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRef2);
+        } else {
+            return;
+        }
+
+        diff.add(builder.build());
+    }
+
+    /**
+     * 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 (RawContactDelta delta : this) {
+            final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
+            if (rawContactId != null && rawContactId >= 0) {
+                return rawContactId;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Find {@link RawContacts#_ID} of the requested {@link RawContactDelta}.
+     */
+    public Long getRawContactId(int index) {
+        if (index >= 0 && index < this.size()) {
+            final RawContactDelta delta = this.get(index);
+            final ValuesDelta values = delta.getValues();
+            if (values.isVisible()) {
+                return values.getAsLong(RawContacts._ID);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Find the raw-contact (an {@link RawContactDelta}) with the specified ID.
+     */
+    public RawContactDelta getByRawContactId(Long rawContactId) {
+        final int index = this.indexOfRawContactId(rawContactId);
+        return (index == -1) ? null : this.get(index);
+    }
+
+    /**
+     * Find index of given {@link RawContacts#_ID} when present.
+     */
+    public int indexOfRawContactId(Long rawContactId) {
+        if (rawContactId == null) return -1;
+        final int size = this.size();
+        for (int i = 0; i < size; i++) {
+            final Long currentId = getRawContactId(i);
+            if (rawContactId.equals(currentId)) {
+                return i;
+            }
+        }
+        return -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 (RawContactDelta delta : this) {
+            if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex;
+            entityIndex++;
+        }
+        return -1;
+    }
+
+    /**  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);
+    }
+
+    public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
+        ValuesDelta primary = null;
+        ValuesDelta randomEntry = null;
+        for (RawContactDelta delta : this) {
+            final ArrayList<ValuesDelta> mimeEntries = delta.getMimeEntries(mimeType);
+            if (mimeEntries == null) return null;
+
+            for (ValuesDelta entry : mimeEntries) {
+                if (entry.isSuperPrimary()) {
+                    return entry;
+                } else if (primary == null && entry.isPrimary()) {
+                    primary = entry;
+                } else if (randomEntry == null) {
+                    randomEntry = entry;
+                }
+            }
+        }
+        // When no direct super primary, return something
+        if (primary != null) {
+            return primary;
+        }
+        return randomEntry;
+    }
+
+    /**
+     * Sets a flag that will split ("explode") the raw_contacts into seperate contacts
+     */
+    public void markRawContactsForSplitting() {
+        mSplitRawContacts = true;
+    }
+
+    public boolean isMarkedForSplitting() {
+        return mSplitRawContacts;
+    }
+
+    public void setJoinWithRawContacts(long[] rawContactIds) {
+        mJoinWithRawContactIds = rawContactIds;
+    }
+
+    public boolean isMarkedForJoining() {
+        return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int describeContents() {
+        // Nothing special about this parcel
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        final int size = this.size();
+        dest.writeInt(size);
+        for (RawContactDelta delta : this) {
+            dest.writeParcelable(delta, flags);
+        }
+        dest.writeLongArray(mJoinWithRawContactIds);
+        dest.writeInt(mSplitRawContacts ? 1 : 0);
+    }
+
+    @SuppressWarnings("unchecked")
+    public void readFromParcel(Parcel source) {
+        final ClassLoader loader = getClass().getClassLoader();
+        final int size = source.readInt();
+        for (int i = 0; i < size; i++) {
+            this.add(source.<RawContactDelta> readParcelable(loader));
+        }
+        mJoinWithRawContactIds = source.createLongArray();
+        mSplitRawContacts = source.readInt() != 0;
+    }
+
+    public static final Parcelable.Creator<RawContactDeltaList> CREATOR =
+            new Parcelable.Creator<RawContactDeltaList>() {
+        @Override
+        public RawContactDeltaList createFromParcel(Parcel in) {
+            final RawContactDeltaList state = new RawContactDeltaList();
+            state.readFromParcel(in);
+            return state;
+        }
+
+        @Override
+        public RawContactDeltaList[] newArray(int size) {
+            return new RawContactDeltaList[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("(");
+        sb.append("Split=");
+        sb.append(mSplitRawContacts);
+        sb.append(", Join=[");
+        sb.append(Arrays.toString(mJoinWithRawContactIds));
+        sb.append("], Values=");
+        sb.append(super.toString());
+        sb.append(")");
+        return sb.toString();
+    }
+}
diff --git a/src/com/android/contacts/common/model/RawContactModifier.java b/src/com/android/contacts/common/model/RawContactModifier.java
new file mode 100644
index 0000000..0cd243c
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactModifier.java
@@ -0,0 +1,1427 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+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.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.Data;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.android.contacts.common.util.DateUtils;
+import com.android.contacts.common.util.NameConverter;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountType.EditField;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.AccountType.EventEditType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Helper methods for modifying an {@link RawContactDelta}, such as inserting
+ * new rows, or enforcing {@link AccountType}.
+ */
+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 RawContactDelta}, determine if the given
+     * {@link DataKind} could be inserted under specific
+     * {@link AccountType}.
+     */
+    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);
+        final boolean validOverall = (kind.typeOverallMax == -1)
+                || (visibleCount < kind.typeOverallMax);
+        return (validTypes && validOverall);
+    }
+
+    public static boolean hasValidTypes(RawContactDelta state, DataKind kind) {
+        if (RawContactModifier.hasEditTypes(kind)) {
+            return (getValidTypes(state, kind).size() > 0);
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Ensure that at least one of the given {@link DataKind} exists in the
+     * 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(
+            RawContactDelta state, AccountType accountType, String mimeType) {
+        final DataKind kind = accountType.getKindForMimetype(mimeType);
+        final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0;
+
+        if (kind != null) {
+            if (hasChild) {
+                // Return the first entry.
+                return state.getMimeEntries(mimeType).get(0);
+            } else {
+                // Create child when none exists and valid kind
+                final ValuesDelta child = insertChild(state, kind);
+                if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
+                    child.setFromTemplate(true);
+                }
+                return child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 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(RawContactDelta state, DataKind kind) {
+        return getValidTypes(state, kind, null, true, null);
+    }
+
+    /**
+     * For the given {@link RawContactDelta} and {@link DataKind}, return the
+     * list possible {@link EditType} options available based on
+     * {@link AccountType}.
+     *
+     * @param forceInclude Always include this {@link EditType} in the returned
+     *            list, even when an otherwise-invalid choice. This is useful
+     *            when showing a dialog that includes the current type.
+     */
+    public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
+            EditType forceInclude) {
+        return getValidTypes(state, kind, forceInclude, true, null);
+    }
+
+    /**
+     * For the given {@link RawContactDelta} and {@link DataKind}, return the
+     * list possible {@link EditType} options available based on
+     * {@link AccountType}.
+     *
+     * @param forceInclude Always include this {@link EditType} in the returned
+     *            list, even when an otherwise-invalid choice. This is useful
+     *            when showing a dialog that includes the current type.
+     * @param includeSecondary If true, include any valid types marked as
+     *            {@link EditType#secondary}.
+     * @param typeCount When provided, will be used for the frequency count of
+     *            each {@link EditType}, otherwise built using
+     *            {@link #getTypeFrequencies(RawContactDelta, DataKind)}.
+     */
+    private static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
+            EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) {
+        final ArrayList<EditType> validTypes = new ArrayList<EditType>();
+
+        // Bail early if no types provided
+        if (!hasEditTypes(kind)) return validTypes;
+
+        if (typeCount == null) {
+            // Build frequency counts if not provided
+            typeCount = getTypeFrequencies(state, kind);
+        }
+
+        // Build list of valid types
+        final int overallCount = typeCount.get(FREQUENCY_TOTAL);
+        for (EditType type : kind.typeList) {
+            final boolean validOverall = (kind.typeOverallMax == -1 ? true
+                    : overallCount < kind.typeOverallMax);
+            final boolean validSpecific = (type.specificMax == -1 ? true : typeCount
+                    .get(type.rawValue) < type.specificMax);
+            final boolean validSecondary = (includeSecondary ? true : !type.secondary);
+            final boolean forcedInclude = type.equals(forceInclude);
+            if (forcedInclude || (validOverall && validSpecific && validSecondary)) {
+                // Type is valid when no limit, under limit, or forced include
+                validTypes.add(type);
+            }
+        }
+
+        return validTypes;
+    }
+
+    private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE;
+
+    /**
+     * Count up the frequency that each {@link EditType} appears in the given
+     * {@link 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(RawContactDelta state, DataKind kind) {
+        final SparseIntArray typeCount = new SparseIntArray();
+
+        // Find all entries for this kind, bailing early if none found
+        final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType);
+        if (mimeEntries == null) return typeCount;
+
+        int totalCount = 0;
+        for (ValuesDelta entry : mimeEntries) {
+            // Only count visible entries
+            if (!entry.isVisible()) continue;
+            totalCount++;
+
+            final EditType type = getCurrentType(entry, kind);
+            if (type != null) {
+                final int count = typeCount.get(type.rawValue);
+                typeCount.put(type.rawValue, count + 1);
+            }
+        }
+        typeCount.put(FREQUENCY_TOTAL, totalCount);
+        return typeCount;
+    }
+
+    /**
+     * Check if the given {@link DataKind} has multiple types that should be
+     * displayed for users to pick.
+     */
+    public static boolean hasEditTypes(DataKind kind) {
+        return kind.typeList != null && kind.typeList.size() > 0;
+    }
+
+    /**
+     * Find the {@link EditType} that describes the given
+     * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates
+     * the possible types.
+     */
+    public static EditType getCurrentType(ValuesDelta entry, DataKind kind) {
+        final Long rawValue = entry.getAsLong(kind.typeColumn);
+        if (rawValue == null) return null;
+        return getType(kind, rawValue.intValue());
+    }
+
+    /**
+     * Find the {@link EditType} that describes the given {@link ContentValues} row,
+     * assuming the given {@link DataKind} dictates the possible types.
+     */
+    public static EditType getCurrentType(ContentValues entry, DataKind kind) {
+        if (kind.typeColumn == null) return null;
+        final Integer rawValue = entry.getAsInteger(kind.typeColumn);
+        if (rawValue == null) return null;
+        return getType(kind, rawValue);
+    }
+
+    /**
+     * Find the {@link EditType} that describes the given {@link Cursor} row,
+     * assuming the given {@link DataKind} dictates the possible types.
+     */
+    public static EditType getCurrentType(Cursor cursor, DataKind kind) {
+        if (kind.typeColumn == null) return null;
+        final int index = cursor.getColumnIndex(kind.typeColumn);
+        if (index == -1) return null;
+        final int rawValue = cursor.getInt(index);
+        return getType(kind, rawValue);
+    }
+
+    /**
+     * Find the {@link EditType} with the given {@link EditType#rawValue}.
+     */
+    public static EditType getType(DataKind kind, int rawValue) {
+        for (EditType type : kind.typeList) {
+            if (type.rawValue == rawValue) {
+                return type;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the precedence for the the given {@link EditType#rawValue}, where
+     * lower numbers are higher precedence.
+     */
+    public static int getTypePrecedence(DataKind kind, int rawValue) {
+        for (int i = 0; i < kind.typeList.size(); i++) {
+            final EditType type = kind.typeList.get(i);
+            if (type.rawValue == rawValue) {
+                return i;
+            }
+        }
+        return Integer.MAX_VALUE;
+    }
+
+    /**
+     * Find the best {@link EditType} for a potential insert. The "best" is the
+     * first primary type that doesn't already exist. When all valid types
+     * exist, we pick the last valid option.
+     */
+    public static EditType getBestValidType(RawContactDelta state, DataKind kind,
+            boolean includeSecondary, int exactValue) {
+        // Shortcut when no types
+        if (kind.typeColumn == null) return null;
+
+        // Find type counts and valid primary types, bail if none
+        final SparseIntArray typeCount = getTypeFrequencies(state, kind);
+        final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary,
+                typeCount);
+        if (validTypes.size() == 0) return null;
+
+        // Keep track of the last valid type
+        final EditType lastType = validTypes.get(validTypes.size() - 1);
+
+        // Remove any types that already exist
+        Iterator<EditType> iterator = validTypes.iterator();
+        while (iterator.hasNext()) {
+            final EditType type = iterator.next();
+            final int count = typeCount.get(type.rawValue);
+
+            if (exactValue == type.rawValue) {
+                // Found exact value match
+                return type;
+            }
+
+            if (count > 0) {
+                // Type already appears, so don't consider
+                iterator.remove();
+            }
+        }
+
+        // Use the best remaining, otherwise the last valid
+        if (validTypes.size() > 0) {
+            return validTypes.get(0);
+        } else {
+            return lastType;
+        }
+    }
+
+    /**
+     * Insert a new child of kind {@link DataKind} into the given
+     * {@link RawContactDelta}. Tries using the best {@link EditType} found using
+     * {@link #getBestValidType(RawContactDelta, DataKind, boolean, int)}.
+     */
+    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) {
+            // No valid primary found, so expand search to secondary
+            bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE);
+        }
+        return insertChild(state, kind, bestType);
+    }
+
+    /**
+     * Insert a new child of kind {@link DataKind} into the given
+     * {@link RawContactDelta}, marked with the given {@link EditType}.
+     */
+    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();
+
+        // Our parent CONTACT_ID is provided later
+        after.put(Data.MIMETYPE, kind.mimeType);
+
+        // Fill-in with any requested default values
+        if (kind.defaultValues != null) {
+            after.putAll(kind.defaultValues);
+        }
+
+        if (kind.typeColumn != null && type != null) {
+            // Set type, if provided
+            after.put(kind.typeColumn, type.rawValue);
+        }
+
+        final ValuesDelta child = ValuesDelta.fromAfter(after);
+        state.addEntry(child);
+        return child;
+    }
+
+    /**
+     * 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(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);
+            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+            trimEmpty(state, type);
+        }
+    }
+
+    public static boolean hasChanges(RawContactDeltaList set, AccountTypeManager accountTypes) {
+        if (set.isMarkedForSplitting() || set.isMarkedForJoining()) {
+            return true;
+        }
+
+        for (RawContactDelta state : set) {
+            ValuesDelta values = state.getValues();
+            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+            final String dataSet = values.getAsString(RawContacts.DATA_SET);
+            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+            if (hasChanges(state, type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Processing to trim any empty {@link ValuesDelta} rows from the given
+     * {@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(RawContactDelta state, AccountType accountType) {
+        boolean hasValues = false;
+
+        // Walk through entries for each well-known kind
+        for (DataKind kind : accountType.getSortedDataKinds()) {
+            final String mimeType = kind.mimeType;
+            final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+            if (entries == null) continue;
+
+            for (ValuesDelta entry : entries) {
+                // Skip any values that haven't been touched
+                final boolean touched = entry.isInsert() || entry.isUpdate();
+                if (!touched) {
+                    hasValues = true;
+                    continue;
+                }
+
+                // Test and remove this row if empty and it isn't a photo from google
+                final boolean isGoogleAccount = TextUtils.equals(GoogleAccountType.ACCOUNT_TYPE,
+                        state.getValues().getAsString(RawContacts.ACCOUNT_TYPE));
+                final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType);
+                final boolean isGooglePhoto = isPhoto && isGoogleAccount;
+
+                if (RawContactModifier.isEmpty(entry, kind) && !isGooglePhoto) {
+                    if (DEBUG) {
+                        Log.v(TAG, "Trimming: " + entry.toString());
+                    }
+                    entry.markDeleted();
+                } else if (!entry.isFromTemplate()) {
+                    hasValues = true;
+                }
+            }
+        }
+        if (!hasValues) {
+            // Trim overall entity if no children exist
+            state.markDeleted();
+        }
+    }
+
+    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);
+            if (entries == null) continue;
+
+            for (ValuesDelta entry : entries) {
+                // An empty Insert must be ignored, because it won't save anything (an example
+                // is an empty name that stays empty)
+                final boolean isRealInsert = entry.isInsert() && !isEmpty(entry, kind);
+                if (isRealInsert || entry.isUpdate() || entry.isDelete()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Test if the given {@link ValuesDelta} would be considered "empty" in
+     * terms of {@link DataKind#fieldList}.
+     */
+    public static boolean isEmpty(ValuesDelta values, DataKind kind) {
+        if (Photo.CONTENT_ITEM_TYPE.equals(kind.mimeType)) {
+            return values.isInsert() && values.getAsByteArray(Photo.PHOTO) == null;
+        }
+
+        // No defined fields mean this row is always empty
+        if (kind.fieldList == null) return true;
+
+        for (EditField field : kind.fieldList) {
+            // If any field has values, we're not empty
+            final String value = values.getAsString(field.column);
+            if (ContactsUtils.isGraphic(value)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares corresponding fields in values1 and values2. Only the fields
+     * declared by the DataKind are taken into consideration.
+     */
+    protected static boolean areEqual(ValuesDelta values1, ContentValues values2, DataKind kind) {
+        if (kind.fieldList == null) return false;
+
+        for (EditField field : kind.fieldList) {
+            final String value1 = values1.getAsString(field.column);
+            final String value2 = values2.getAsString(field.column);
+            if (!TextUtils.equals(value1, value2)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * 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, RawContactDelta state,
+            Bundle extras) {
+        if (extras == null || extras.size() == 0) {
+            // Bail early if no useful data
+            return;
+        }
+
+        parseStructuredNameExtra(context, accountType, state, extras);
+        parseStructuredPostalExtra(accountType, state, extras);
+
+        {
+            // Phone
+            final DataKind kind = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+            parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER);
+            parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE,
+                    Phone.NUMBER);
+            parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE,
+                    Phone.NUMBER);
+        }
+
+        {
+            // Email
+            final DataKind kind = accountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
+            parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.EMAIL, Email.DATA);
+            parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL,
+                    Email.DATA);
+            parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL,
+                    Email.DATA);
+        }
+
+        {
+            // Im
+            final DataKind kind = accountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
+            fixupLegacyImType(extras);
+            parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA);
+        }
+
+        // Organization
+        final boolean hasOrg = extras.containsKey(Insert.COMPANY)
+                || extras.containsKey(Insert.JOB_TITLE);
+        final DataKind kindOrg = accountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
+        if (hasOrg && RawContactModifier.canInsert(state, kindOrg)) {
+            final ValuesDelta child = RawContactModifier.insertChild(state, kindOrg);
+
+            final String company = extras.getString(Insert.COMPANY);
+            if (ContactsUtils.isGraphic(company)) {
+                child.put(Organization.COMPANY, company);
+            }
+
+            final String title = extras.getString(Insert.JOB_TITLE);
+            if (ContactsUtils.isGraphic(title)) {
+                child.put(Organization.TITLE, title);
+            }
+        }
+
+        // Notes
+        final boolean hasNotes = extras.containsKey(Insert.NOTES);
+        final DataKind kindNotes = accountType.getKindForMimetype(Note.CONTENT_ITEM_TYPE);
+        if (hasNotes && RawContactModifier.canInsert(state, kindNotes)) {
+            final ValuesDelta child = RawContactModifier.insertChild(state, kindNotes);
+
+            final String notes = extras.getString(Insert.NOTES);
+            if (ContactsUtils.isGraphic(notes)) {
+                child.put(Note.NOTE, notes);
+            }
+        }
+
+        // Arbitrary additional data
+        ArrayList<ContentValues> values = extras.getParcelableArrayList(Insert.DATA);
+        if (values != null) {
+            parseValues(state, accountType, values);
+        }
+    }
+
+    private static void parseStructuredNameExtra(
+            Context context, AccountType accountType, RawContactDelta state, Bundle extras) {
+        // StructuredName
+        RawContactModifier.ensureKindExists(state, accountType, StructuredName.CONTENT_ITEM_TYPE);
+        final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
+
+        final String name = extras.getString(Insert.NAME);
+        if (ContactsUtils.isGraphic(name)) {
+            final DataKind kind = accountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+            boolean supportsDisplayName = false;
+            if (kind.fieldList != null) {
+                for (EditField field : kind.fieldList) {
+                    if (StructuredName.DISPLAY_NAME.equals(field.column)) {
+                        supportsDisplayName = true;
+                        break;
+                    }
+                }
+            }
+
+            if (supportsDisplayName) {
+                child.put(StructuredName.DISPLAY_NAME, name);
+            } else {
+                Uri uri = ContactsContract.AUTHORITY_URI.buildUpon()
+                        .appendPath("complete_name")
+                        .appendQueryParameter(StructuredName.DISPLAY_NAME, name)
+                        .build();
+                Cursor cursor = context.getContentResolver().query(uri,
+                        new String[]{
+                                StructuredName.PREFIX,
+                                StructuredName.GIVEN_NAME,
+                                StructuredName.MIDDLE_NAME,
+                                StructuredName.FAMILY_NAME,
+                                StructuredName.SUFFIX,
+                        }, null, null, null);
+
+                try {
+                    if (cursor.moveToFirst()) {
+                        child.put(StructuredName.PREFIX, cursor.getString(0));
+                        child.put(StructuredName.GIVEN_NAME, cursor.getString(1));
+                        child.put(StructuredName.MIDDLE_NAME, cursor.getString(2));
+                        child.put(StructuredName.FAMILY_NAME, cursor.getString(3));
+                        child.put(StructuredName.SUFFIX, cursor.getString(4));
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        final String phoneticName = extras.getString(Insert.PHONETIC_NAME);
+        if (ContactsUtils.isGraphic(phoneticName)) {
+            child.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticName);
+        }
+    }
+
+    private static void parseStructuredPostalExtra(
+            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,
+                Insert.POSTAL, StructuredPostal.FORMATTED_ADDRESS);
+        String address = child == null ? null
+                : child.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+        if (!TextUtils.isEmpty(address)) {
+            boolean supportsFormatted = false;
+            if (kind.fieldList != null) {
+                for (EditField field : kind.fieldList) {
+                    if (StructuredPostal.FORMATTED_ADDRESS.equals(field.column)) {
+                        supportsFormatted = true;
+                        break;
+                    }
+                }
+            }
+
+            if (!supportsFormatted) {
+                child.put(StructuredPostal.STREET, address);
+                child.putNull(StructuredPostal.FORMATTED_ADDRESS);
+            }
+        }
+    }
+
+    private static void parseValues(
+            RawContactDelta state, AccountType accountType,
+            ArrayList<ContentValues> dataValueList) {
+        for (ContentValues values : dataValueList) {
+            String mimeType = values.getAsString(Data.MIMETYPE);
+            if (TextUtils.isEmpty(mimeType)) {
+                Log.e(TAG, "Mimetype is required. Ignoring: " + values);
+                continue;
+            }
+
+            // Won't override the contact name
+            if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                continue;
+            } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                values.remove(PhoneDataItem.KEY_FORMATTED_PHONE_NUMBER);
+                final Integer type = values.getAsInteger(Phone.TYPE);
+                // If the provided phone number provides a custom phone type but not a label,
+                // replace it with mobile (by default) to avoid the "Enter custom label" from
+                // popping up immediately upon entering the ContactEditorFragment
+                if (type != null && type == Phone.TYPE_CUSTOM &&
+                        TextUtils.isEmpty(values.getAsString(Phone.LABEL))) {
+                    values.put(Phone.TYPE, Phone.TYPE_MOBILE);
+                }
+            }
+
+            DataKind kind = accountType.getKindForMimetype(mimeType);
+            if (kind == null) {
+                Log.e(TAG, "Mimetype not supported for account type "
+                        + accountType.getAccountTypeAndDataSet() + ". Ignoring: " + values);
+                continue;
+            }
+
+            ValuesDelta entry = ValuesDelta.fromAfter(values);
+            if (isEmpty(entry, kind)) {
+                continue;
+            }
+
+            ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+
+            if ((kind.typeOverallMax != 1) || GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                // Check for duplicates
+                boolean addEntry = true;
+                int count = 0;
+                if (entries != null && entries.size() > 0) {
+                    for (ValuesDelta delta : entries) {
+                        if (!delta.isDelete()) {
+                            if (areEqual(delta, values, kind)) {
+                                addEntry = false;
+                                break;
+                            }
+                            count++;
+                        }
+                    }
+                }
+
+                if (kind.typeOverallMax != -1 && count >= kind.typeOverallMax) {
+                    Log.e(TAG, "Mimetype allows at most " + kind.typeOverallMax
+                            + " entries. Ignoring: " + values);
+                    addEntry = false;
+                }
+
+                if (addEntry) {
+                    addEntry = adjustType(entry, entries, kind);
+                }
+
+                if (addEntry) {
+                    state.addEntry(entry);
+                }
+            } else {
+                // Non-list entries should not be overridden
+                boolean addEntry = true;
+                if (entries != null && entries.size() > 0) {
+                    for (ValuesDelta delta : entries) {
+                        if (!delta.isDelete() && !isEmpty(delta, kind)) {
+                            addEntry = false;
+                            break;
+                        }
+                    }
+                    if (addEntry) {
+                        for (ValuesDelta delta : entries) {
+                            delta.markDeleted();
+                        }
+                    }
+                }
+
+                if (addEntry) {
+                    addEntry = adjustType(entry, entries, kind);
+                }
+
+                if (addEntry) {
+                    state.addEntry(entry);
+                } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)){
+                    // Note is most likely to contain large amounts of text
+                    // that we don't want to drop on the ground.
+                    for (ValuesDelta delta : entries) {
+                        if (!isEmpty(delta, kind)) {
+                            delta.put(Note.NOTE, delta.getAsString(Note.NOTE) + "\n"
+                                    + values.getAsString(Note.NOTE));
+                            break;
+                        }
+                    }
+                } else {
+                    Log.e(TAG, "Will not override mimetype " + mimeType + ". Ignoring: "
+                            + values);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if the data kind allows addition of another entry (e.g. Exchange only
+     * supports two "work" phone numbers).  If not, tries to switch to one of the
+     * unused types.  If successful, returns true.
+     */
+    private static boolean adjustType(
+            ValuesDelta entry, ArrayList<ValuesDelta> entries, DataKind kind) {
+        if (kind.typeColumn == null || kind.typeList == null || kind.typeList.size() == 0) {
+            return true;
+        }
+
+        Integer typeInteger = entry.getAsInteger(kind.typeColumn);
+        int type = typeInteger != null ? typeInteger : kind.typeList.get(0).rawValue;
+
+        if (isTypeAllowed(type, entries, kind)) {
+            entry.put(kind.typeColumn, type);
+            return true;
+        }
+
+        // Specified type is not allowed - choose the first available type that is allowed
+        int size = kind.typeList.size();
+        for (int i = 0; i < size; i++) {
+            EditType editType = kind.typeList.get(i);
+            if (isTypeAllowed(editType.rawValue, entries, kind)) {
+                entry.put(kind.typeColumn, editType.rawValue);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks if a new entry of the specified type can be added to the raw
+     * contact. For example, Exchange only supports two "work" phone numbers, so
+     * addition of a third would not be allowed.
+     */
+    private static boolean isTypeAllowed(int type, ArrayList<ValuesDelta> entries, DataKind kind) {
+        int max = 0;
+        int size = kind.typeList.size();
+        for (int i = 0; i < size; i++) {
+            EditType editType = kind.typeList.get(i);
+            if (editType.rawValue == type) {
+                max = editType.specificMax;
+                break;
+            }
+        }
+
+        if (max == 0) {
+            // This type is not allowed at all
+            return false;
+        }
+
+        if (max == -1) {
+            // Unlimited instances of this type are allowed
+            return true;
+        }
+
+        return getEntryCountByType(entries, kind.typeColumn, type) < max;
+    }
+
+    /**
+     * Counts occurrences of the specified type in the supplied entry list.
+     *
+     * @return The count of occurrences of the type in the entry list. 0 if entries is
+     * {@literal null}
+     */
+    private static int getEntryCountByType(ArrayList<ValuesDelta> entries, String typeColumn,
+            int type) {
+        int count = 0;
+        if (entries != null) {
+            for (ValuesDelta entry : entries) {
+                Integer typeInteger = entry.getAsInteger(typeColumn);
+                if (typeInteger != null && typeInteger == type) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them
+     * with updated values.
+     */
+    @SuppressWarnings("deprecation")
+    private static void fixupLegacyImType(Bundle bundle) {
+        final String encodedString = bundle.getString(Insert.IM_PROTOCOL);
+        if (encodedString == null) return;
+
+        try {
+            final Object protocol = android.provider.Contacts.ContactMethods
+                    .decodeImProtocol(encodedString);
+            if (protocol instanceof Integer) {
+                bundle.putInt(Insert.IM_PROTOCOL, (Integer)protocol);
+            } else {
+                bundle.putString(Insert.IM_PROTOCOL, (String)protocol);
+            }
+        } catch (IllegalArgumentException e) {
+            // Ignore exception when legacy parser fails
+        }
+    }
+
+    /**
+     * Parse a specific entry from the given {@link Bundle} and insert into the
+     * 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
+     *            {@link EditType#rawValue} value.
+     * @param valueExtra {@link Bundle} key that holds the incoming value.
+     * @param valueColumn Column to write value into {@link ValuesDelta}.
+     */
+    public static ValuesDelta parseExtras(RawContactDelta state, DataKind kind, Bundle extras,
+            String typeExtra, String valueExtra, String valueColumn) {
+        final CharSequence value = extras.getCharSequence(valueExtra);
+
+        // Bail early if account type doesn't handle this MIME type
+        if (kind == null) return null;
+
+        // Bail when can't insert type, or value missing
+        final boolean canInsert = RawContactModifier.canInsert(state, kind);
+        final boolean validValue = (value != null && TextUtils.isGraphic(value));
+        if (!validValue || !canInsert) return null;
+
+        // Find exact type when requested, otherwise best available type
+        final boolean hasType = extras.containsKey(typeExtra);
+        final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM
+                : Integer.MIN_VALUE);
+        final EditType editType = RawContactModifier.getBestValidType(state, kind, true, typeValue);
+
+        // Create data row and fill with value
+        final ValuesDelta child = RawContactModifier.insertChild(state, kind, editType);
+        child.put(valueColumn, value.toString());
+
+        if (editType != null && editType.customColumn != null) {
+            // Write down label when custom type picked
+            final String customType = extras.getString(typeExtra);
+            child.put(editType.customColumn, customType);
+        }
+
+        return child;
+    }
+
+    /**
+     * Generic mime types with type support (e.g. TYPE_HOME).
+     * Here, "type support" means if the data kind has CommonColumns#TYPE or not. Data kinds which
+     * have their own migrate methods aren't listed here.
+     */
+    private static final Set<String> sGenericMimeTypesWithTypeSupport = new HashSet<String>(
+            Arrays.asList(Phone.CONTENT_ITEM_TYPE,
+                    Email.CONTENT_ITEM_TYPE,
+                    Im.CONTENT_ITEM_TYPE,
+                    Nickname.CONTENT_ITEM_TYPE,
+                    Website.CONTENT_ITEM_TYPE,
+                    Relation.CONTENT_ITEM_TYPE,
+                    SipAddress.CONTENT_ITEM_TYPE));
+    private static final Set<String> sGenericMimeTypesWithoutTypeSupport = new HashSet<String>(
+            Arrays.asList(Organization.CONTENT_ITEM_TYPE,
+                    Note.CONTENT_ITEM_TYPE,
+                    Photo.CONTENT_ITEM_TYPE,
+                    GroupMembership.CONTENT_ITEM_TYPE));
+    // CommonColumns.TYPE cannot be accessed as it is protected interface, so use
+    // Phone.TYPE instead.
+    private static final String COLUMN_FOR_TYPE  = Phone.TYPE;
+    private static final String COLUMN_FOR_LABEL  = Phone.LABEL;
+    private static final int TYPE_CUSTOM = Phone.TYPE_CUSTOM;
+
+    /**
+     * 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,
+            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
+            // shortcuts.
+            for (DataKind kind : newAccountType.getSortedDataKinds()) {
+                final String mimeType = kind.mimeType;
+                // The fields with short/long form capability must be treated properly.
+                if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    migrateStructuredName(context, oldState, newState, kind);
+                } else {
+                    List<ValuesDelta> entryList = oldState.getMimeEntries(mimeType);
+                    if (entryList != null && !entryList.isEmpty()) {
+                        for (ValuesDelta entry : entryList) {
+                            ContentValues values = entry.getAfter();
+                            if (values != null) {
+                                newState.addEntry(ValuesDelta.fromAfter(values));
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            // Migrate data supported by the new account type.
+            // All the other data inside oldState are silently dropped.
+            for (DataKind kind : newAccountType.getSortedDataKinds()) {
+                if (!kind.editable) continue;
+                final String mimeType = kind.mimeType;
+                if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
+                        || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
+                    // Ignore pseudo data.
+                    continue;
+                } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    migrateStructuredName(context, oldState, newState, kind);
+                } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    migratePostal(oldState, newState, kind);
+                } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    migrateEvent(oldState, newState, kind, null /* default Year */);
+                } else if (sGenericMimeTypesWithoutTypeSupport.contains(mimeType)) {
+                    migrateGenericWithoutTypeColumn(oldState, newState, kind);
+                } else if (sGenericMimeTypesWithTypeSupport.contains(mimeType)) {
+                    migrateGenericWithTypeColumn(oldState, newState, kind);
+                } else {
+                    throw new IllegalStateException("Unexpected editable mime-type: " + mimeType);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks {@link DataKind#isList} and {@link DataKind#typeOverallMax}, and restricts
+     * the number of entries (ValuesDelta) inside newState.
+     */
+    private static ArrayList<ValuesDelta> ensureEntryMaxSize(RawContactDelta newState,
+            DataKind kind, ArrayList<ValuesDelta> mimeEntries) {
+        if (mimeEntries == null) {
+            return null;
+        }
+
+        final int typeOverallMax = kind.typeOverallMax;
+        if (typeOverallMax >= 0 && (mimeEntries.size() > typeOverallMax)) {
+            ArrayList<ValuesDelta> newMimeEntries = new ArrayList<ValuesDelta>(typeOverallMax);
+            for (int i = 0; i < typeOverallMax; i++) {
+                newMimeEntries.add(mimeEntries.get(i));
+            }
+            mimeEntries = newMimeEntries;
+        }
+        return mimeEntries;
+    }
+
+    /** @hide Public only for testing. */
+    public static void migrateStructuredName(
+            Context context, RawContactDelta oldState, RawContactDelta newState,
+            DataKind newDataKind) {
+        final ContentValues values =
+                oldState.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE).getAfter();
+        if (values == null) {
+            return;
+        }
+
+        boolean supportDisplayName = false;
+        boolean supportPhoneticFullName = false;
+        boolean supportPhoneticFamilyName = false;
+        boolean supportPhoneticMiddleName = false;
+        boolean supportPhoneticGivenName = false;
+        for (EditField editField : newDataKind.fieldList) {
+            if (StructuredName.DISPLAY_NAME.equals(editField.column)) {
+                supportDisplayName = true;
+            }
+            if (DataKind.PSEUDO_COLUMN_PHONETIC_NAME.equals(editField.column)) {
+                supportPhoneticFullName = true;
+            }
+            if (StructuredName.PHONETIC_FAMILY_NAME.equals(editField.column)) {
+                supportPhoneticFamilyName = true;
+            }
+            if (StructuredName.PHONETIC_MIDDLE_NAME.equals(editField.column)) {
+                supportPhoneticMiddleName = true;
+            }
+            if (StructuredName.PHONETIC_GIVEN_NAME.equals(editField.column)) {
+                supportPhoneticGivenName = true;
+            }
+        }
+
+        // DISPLAY_NAME <-> PREFIX, GIVEN_NAME, MIDDLE_NAME, FAMILY_NAME, SUFFIX
+        final String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
+        if (!TextUtils.isEmpty(displayName)) {
+            if (!supportDisplayName) {
+                // Old data has a display name, while the new account doesn't allow it.
+                NameConverter.displayNameToStructuredName(context, displayName, values);
+
+                // We don't want to migrate unseen data which may confuse users after the creation.
+                values.remove(StructuredName.DISPLAY_NAME);
+            }
+        } else {
+            if (supportDisplayName) {
+                // Old data does not have display name, while the new account requires it.
+                values.put(StructuredName.DISPLAY_NAME,
+                        NameConverter.structuredNameToDisplayName(context, values));
+                for (String field : NameConverter.STRUCTURED_NAME_FIELDS) {
+                    values.remove(field);
+                }
+            }
+        }
+
+        // Phonetic (full) name <-> PHONETIC_FAMILY_NAME, PHONETIC_MIDDLE_NAME, PHONETIC_GIVEN_NAME
+        final String phoneticFullName = values.getAsString(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
+        if (!TextUtils.isEmpty(phoneticFullName)) {
+            if (!supportPhoneticFullName) {
+                // Old data has a phonetic (full) name, while the new account doesn't allow it.
+                final StructuredNameDataItem tmpItem =
+                        NameConverter.parsePhoneticName(phoneticFullName, null);
+                values.remove(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
+                if (supportPhoneticFamilyName) {
+                    values.put(StructuredName.PHONETIC_FAMILY_NAME,
+                            tmpItem.getPhoneticFamilyName());
+                } else {
+                    values.remove(StructuredName.PHONETIC_FAMILY_NAME);
+                }
+                if (supportPhoneticMiddleName) {
+                    values.put(StructuredName.PHONETIC_MIDDLE_NAME,
+                            tmpItem.getPhoneticMiddleName());
+                } else {
+                    values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
+                }
+                if (supportPhoneticGivenName) {
+                    values.put(StructuredName.PHONETIC_GIVEN_NAME,
+                            tmpItem.getPhoneticGivenName());
+                } else {
+                    values.remove(StructuredName.PHONETIC_GIVEN_NAME);
+                }
+            }
+        } else {
+            if (supportPhoneticFullName) {
+                // Old data does not have a phonetic (full) name, while the new account requires it.
+                values.put(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
+                        NameConverter.buildPhoneticName(
+                                values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
+                                values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
+                                values.getAsString(StructuredName.PHONETIC_GIVEN_NAME)));
+            }
+            if (!supportPhoneticFamilyName) {
+                values.remove(StructuredName.PHONETIC_FAMILY_NAME);
+            }
+            if (!supportPhoneticMiddleName) {
+                values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
+            }
+            if (!supportPhoneticGivenName) {
+                values.remove(StructuredName.PHONETIC_GIVEN_NAME);
+            }
+        }
+
+        newState.addEntry(ValuesDelta.fromAfter(values));
+    }
+
+    /** @hide Public only for testing. */
+    public static void migratePostal(RawContactDelta oldState, RawContactDelta newState,
+            DataKind newDataKind) {
+        final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
+                oldState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
+        if (mimeEntries == null || mimeEntries.isEmpty()) {
+            return;
+        }
+
+        boolean supportFormattedAddress = false;
+        boolean supportStreet = false;
+        final String firstColumn = newDataKind.fieldList.get(0).column;
+        for (EditField editField : newDataKind.fieldList) {
+            if (StructuredPostal.FORMATTED_ADDRESS.equals(editField.column)) {
+                supportFormattedAddress = true;
+            }
+            if (StructuredPostal.STREET.equals(editField.column)) {
+                supportStreet = true;
+            }
+        }
+
+        final Set<Integer> supportedTypes = new HashSet<Integer>();
+        if (newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
+            for (EditType editType : newDataKind.typeList) {
+                supportedTypes.add(editType.rawValue);
+            }
+        }
+
+        for (ValuesDelta entry : mimeEntries) {
+            final ContentValues values = entry.getAfter();
+            if (values == null) {
+                continue;
+            }
+            final Integer oldType = values.getAsInteger(StructuredPostal.TYPE);
+            if (!supportedTypes.contains(oldType)) {
+                int defaultType;
+                if (newDataKind.defaultValues != null) {
+                    defaultType = newDataKind.defaultValues.getAsInteger(StructuredPostal.TYPE);
+                } else {
+                    defaultType = newDataKind.typeList.get(0).rawValue;
+                }
+                values.put(StructuredPostal.TYPE, defaultType);
+                if (oldType != null && oldType == StructuredPostal.TYPE_CUSTOM) {
+                    values.remove(StructuredPostal.LABEL);
+                }
+            }
+
+            final String formattedAddress = values.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+            if (!TextUtils.isEmpty(formattedAddress)) {
+                if (!supportFormattedAddress) {
+                    // Old data has a formatted address, while the new account doesn't allow it.
+                    values.remove(StructuredPostal.FORMATTED_ADDRESS);
+
+                    // Unlike StructuredName we don't have logic to split it, so first
+                    // try to use street field and. If the new account doesn't have one,
+                    // then select first one anyway.
+                    if (supportStreet) {
+                        values.put(StructuredPostal.STREET, formattedAddress);
+                    } else {
+                        values.put(firstColumn, formattedAddress);
+                    }
+                }
+            } else {
+                if (supportFormattedAddress) {
+                    // Old data does not have formatted address, while the new account requires it.
+                    // Unlike StructuredName we don't have logic to join multiple address values.
+                    // Use poor join heuristics for now.
+                    String[] structuredData;
+                    final boolean useJapaneseOrder =
+                            Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+                    if (useJapaneseOrder) {
+                        structuredData = new String[] {
+                                values.getAsString(StructuredPostal.COUNTRY),
+                                values.getAsString(StructuredPostal.POSTCODE),
+                                values.getAsString(StructuredPostal.REGION),
+                                values.getAsString(StructuredPostal.CITY),
+                                values.getAsString(StructuredPostal.NEIGHBORHOOD),
+                                values.getAsString(StructuredPostal.STREET),
+                                values.getAsString(StructuredPostal.POBOX) };
+                    } else {
+                        structuredData = new String[] {
+                                values.getAsString(StructuredPostal.POBOX),
+                                values.getAsString(StructuredPostal.STREET),
+                                values.getAsString(StructuredPostal.NEIGHBORHOOD),
+                                values.getAsString(StructuredPostal.CITY),
+                                values.getAsString(StructuredPostal.REGION),
+                                values.getAsString(StructuredPostal.POSTCODE),
+                                values.getAsString(StructuredPostal.COUNTRY) };
+                    }
+                    final StringBuilder builder = new StringBuilder();
+                    for (String elem : structuredData) {
+                        if (!TextUtils.isEmpty(elem)) {
+                            builder.append(elem + "\n");
+                        }
+                    }
+                    values.put(StructuredPostal.FORMATTED_ADDRESS, builder.toString());
+
+                    values.remove(StructuredPostal.POBOX);
+                    values.remove(StructuredPostal.STREET);
+                    values.remove(StructuredPostal.NEIGHBORHOOD);
+                    values.remove(StructuredPostal.CITY);
+                    values.remove(StructuredPostal.REGION);
+                    values.remove(StructuredPostal.POSTCODE);
+                    values.remove(StructuredPostal.COUNTRY);
+                }
+            }
+
+            newState.addEntry(ValuesDelta.fromAfter(values));
+        }
+    }
+
+    /** @hide Public only for testing. */
+    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));
+        if (mimeEntries == null || mimeEntries.isEmpty()) {
+            return;
+        }
+
+        final SparseArray<EventEditType> allowedTypes = new SparseArray<EventEditType>();
+        for (EditType editType : newDataKind.typeList) {
+            allowedTypes.put(editType.rawValue, (EventEditType) editType);
+        }
+        for (ValuesDelta entry : mimeEntries) {
+            final ContentValues values = entry.getAfter();
+            if (values == null) {
+                continue;
+            }
+            final String dateString = values.getAsString(Event.START_DATE);
+            final Integer type = values.getAsInteger(Event.TYPE);
+            if (type != null && (allowedTypes.indexOfKey(type) >= 0)
+                    && !TextUtils.isEmpty(dateString)) {
+                EventEditType suitableType = allowedTypes.get(type);
+
+                final ParsePosition position = new ParsePosition(0);
+                boolean yearOptional = false;
+                Date date = CommonDateUtils.DATE_AND_TIME_FORMAT.parse(dateString, position);
+                if (date == null) {
+                    yearOptional = true;
+                    date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(dateString, position);
+                }
+                if (date != null) {
+                    if (yearOptional && !suitableType.isYearOptional()) {
+                        // The new EditType doesn't allow optional year. Supply default.
+                        final Calendar calendar = Calendar.getInstance(DateUtils.UTC_TIMEZONE,
+                                Locale.US);
+                        if (defaultYear == null) {
+                            defaultYear = calendar.get(Calendar.YEAR);
+                        }
+                        calendar.setTime(date);
+                        final int month = calendar.get(Calendar.MONTH);
+                        final int day = calendar.get(Calendar.DAY_OF_MONTH);
+                        // Exchange requires 8:00 for birthdays
+                        calendar.set(defaultYear, month, day,
+                                CommonDateUtils.DEFAULT_HOUR, 0, 0);
+                        values.put(Event.START_DATE,
+                                CommonDateUtils.FULL_DATE_FORMAT.format(calendar.getTime()));
+                    }
+                }
+                newState.addEntry(ValuesDelta.fromAfter(values));
+            } else {
+                // Just drop it.
+            }
+        }
+    }
+
+    /** @hide Public only for testing. */
+    public static void migrateGenericWithoutTypeColumn(
+            RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
+        final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
+                oldState.getMimeEntries(newDataKind.mimeType));
+        if (mimeEntries == null || mimeEntries.isEmpty()) {
+            return;
+        }
+
+        for (ValuesDelta entry : mimeEntries) {
+            ContentValues values = entry.getAfter();
+            if (values != null) {
+                newState.addEntry(ValuesDelta.fromAfter(values));
+            }
+        }
+    }
+
+    /** @hide Public only for testing. */
+    public static void migrateGenericWithTypeColumn(
+            RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
+        final ArrayList<ValuesDelta> mimeEntries = oldState.getMimeEntries(newDataKind.mimeType);
+        if (mimeEntries == null || mimeEntries.isEmpty()) {
+            return;
+        }
+
+        // Note that type specified with the old account may be invalid with the new account, while
+        // we want to preserve its data as much as possible. e.g. if a user typed a phone number
+        // with a type which is valid with an old account but not with a new account, the user
+        // probably wants to have the number with default type, rather than seeing complete data
+        // loss.
+        //
+        // Specifically, this method works as follows:
+        // 1. detect defaultType
+        // 2. prepare constants & variables for iteration
+        // 3. iterate over mimeEntries:
+        // 3.1 stop iteration if total number of mimeEntries reached typeOverallMax specified in
+        //     DataKind
+        // 3.2 replace unallowed types with defaultType
+        // 3.3 check if the number of entries is below specificMax specified in AccountType
+
+        // Here, defaultType can be supplied in two ways
+        // - via kind.defaultValues
+        // - via kind.typeList.get(0).rawValue
+        Integer defaultType = null;
+        if (newDataKind.defaultValues != null) {
+            defaultType = newDataKind.defaultValues.getAsInteger(COLUMN_FOR_TYPE);
+        }
+        final Set<Integer> allowedTypes = new HashSet<Integer>();
+        // key: type, value: the number of entries allowed for the type (specificMax)
+        final SparseIntArray typeSpecificMaxMap = new SparseIntArray();
+        if (defaultType != null) {
+            allowedTypes.add(defaultType);
+            typeSpecificMaxMap.put(defaultType, -1);
+        }
+        // Note: typeList may be used in different purposes when defaultValues are specified.
+        // Especially in IM, typeList contains available protocols (e.g. PROTOCOL_GOOGLE_TALK)
+        // instead of "types" which we want to treate here (e.g. TYPE_HOME). So we don't add
+        // anything other than defaultType into allowedTypes and typeSpecificMapMax.
+        if (!Im.CONTENT_ITEM_TYPE.equals(newDataKind.mimeType) &&
+                newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
+            for (EditType editType : newDataKind.typeList) {
+                allowedTypes.add(editType.rawValue);
+                typeSpecificMaxMap.put(editType.rawValue, editType.specificMax);
+            }
+            if (defaultType == null) {
+                defaultType = newDataKind.typeList.get(0).rawValue;
+            }
+        }
+
+        if (defaultType == null) {
+            Log.w(TAG, "Default type isn't available for mimetype " + newDataKind.mimeType);
+        }
+
+        final int typeOverallMax = newDataKind.typeOverallMax;
+
+        // key: type, value: the number of current entries.
+        final SparseIntArray currentEntryCount = new SparseIntArray();
+        int totalCount = 0;
+
+        for (ValuesDelta entry : mimeEntries) {
+            if (typeOverallMax != -1 && totalCount >= typeOverallMax) {
+                break;
+            }
+
+            final ContentValues values = entry.getAfter();
+            if (values == null) {
+                continue;
+            }
+
+            final Integer oldType = entry.getAsInteger(COLUMN_FOR_TYPE);
+            final Integer typeForNewAccount;
+            if (!allowedTypes.contains(oldType)) {
+                // The new account doesn't support the type.
+                if (defaultType != null) {
+                    typeForNewAccount = defaultType.intValue();
+                    values.put(COLUMN_FOR_TYPE, defaultType.intValue());
+                    if (oldType != null && oldType == TYPE_CUSTOM) {
+                        values.remove(COLUMN_FOR_LABEL);
+                    }
+                } else {
+                    typeForNewAccount = null;
+                    values.remove(COLUMN_FOR_TYPE);
+                }
+            } else {
+                typeForNewAccount = oldType;
+            }
+            if (typeForNewAccount != null) {
+                final int specificMax = typeSpecificMaxMap.get(typeForNewAccount, 0);
+                if (specificMax >= 0) {
+                    final int currentCount = currentEntryCount.get(typeForNewAccount, 0);
+                    if (currentCount >= specificMax) {
+                        continue;
+                    }
+                    currentEntryCount.put(typeForNewAccount, currentCount + 1);
+                }
+            }
+            newState.addEntry(ValuesDelta.fromAfter(values));
+            totalCount++;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/DataItem.java b/src/com/android/contacts/common/model/dataitem/DataItem.java
new file mode 100644
index 0000000..60a006f
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/DataItem.java
@@ -0,0 +1,162 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+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.common.model.dataitem.DataKind;
+
+/**
+ * This is the base class for data items, which represents a row from the Data table.
+ */
+public class DataItem {
+
+    private final ContentValues mContentValues;
+
+    protected DataItem(ContentValues values) {
+        mContentValues = values;
+    }
+
+    /**
+     * 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(ContentValues values) {
+        final String mimeType = values.getAsString(Data.MIMETYPE);
+        if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new GroupMembershipDataItem(values);
+        } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new StructuredNameDataItem(values);
+        } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new PhoneDataItem(values);
+        } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new EmailDataItem(values);
+        } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new StructuredPostalDataItem(values);
+        } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new ImDataItem(values);
+        } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new OrganizationDataItem(values);
+        } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new NicknameDataItem(values);
+        } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new NoteDataItem(values);
+        } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new WebsiteDataItem(values);
+        } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new SipAddressDataItem(values);
+        } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new EventDataItem(values);
+        } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new RelationDataItem(values);
+        } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new IdentityDataItem(values);
+        } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new PhotoDataItem(values);
+        }
+
+        // generic
+        return new DataItem(values);
+    }
+
+    public ContentValues getContentValues() {
+        return mContentValues;
+    }
+
+    public void setRawContactId(long rawContactId) {
+        mContentValues.put(Data.RAW_CONTACT_ID, rawContactId);
+    }
+
+    /**
+     * Returns the data id.
+     */
+    public long getId() {
+        return mContentValues.getAsLong(Data._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 boolean hasKindTypeColumn(DataKind kind) {
+        final String key = kind.typeColumn;
+        return key != null && mContentValues.containsKey(key) &&
+            mContentValues.getAsInteger(key) != null;
+    }
+
+    public int getKindTypeColumn(DataKind kind) {
+        final String key = kind.typeColumn;
+        return mContentValues.getAsInteger(key);
+    }
+
+    /**
+     * This builds the data string depending on the type of data item by using the generic
+     * DataKind object underneath.
+     */
+    public String buildDataString(Context context, DataKind kind) {
+        if (kind.actionBody == null) {
+            return null;
+        }
+        CharSequence actionBody = kind.actionBody.inflateUsing(context, mContentValues);
+        return actionBody == null ? null : actionBody.toString();
+    }
+
+    /**
+     * This builds the data string(intended for display) depending on the type of data item. It
+     * returns the same value as {@link #buildDataString} by default, but certain data items can
+     * override it to provide their version of formatted data strings.
+     *
+     * @return Data string representing the data item, possibly formatted for display
+     */
+    public String buildDataStringForDisplay(Context context, DataKind kind) {
+        return buildDataString(context, kind);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/EmailDataItem.java b/src/com/android/contacts/common/model/dataitem/EmailDataItem.java
new file mode 100644
index 0000000..23efb01
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/EmailDataItem.java
@@ -0,0 +1,48 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+/**
+ * Represents an email data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Email}.
+ */
+public class EmailDataItem extends DataItem {
+
+    /* package */ EmailDataItem(ContentValues values) {
+        super(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);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Email.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/EventDataItem.java b/src/com/android/contacts/common/model/dataitem/EventDataItem.java
new file mode 100644
index 0000000..e664db1
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/EventDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+
+/**
+ * Represents an event data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Event}.
+ */
+public class EventDataItem extends DataItem {
+
+    /* package */ EventDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getStartDate() {
+        return getContentValues().getAsString(Event.START_DATE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Event.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java b/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
new file mode 100644
index 0000000..41f19e6
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+
+/**
+ * Represents a group memebership data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.GroupMembership}.
+ */
+public class GroupMembershipDataItem extends DataItem {
+
+    /* package */ GroupMembershipDataItem(ContentValues values) {
+        super(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/common/model/dataitem/IdentityDataItem.java b/src/com/android/contacts/common/model/dataitem/IdentityDataItem.java
new file mode 100644
index 0000000..29e9a40
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/IdentityDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+
+/**
+ * Represents an identity data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Identity}.
+ */
+public class IdentityDataItem extends DataItem {
+
+    /* package */ IdentityDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getIdentity() {
+        return getContentValues().getAsString(Identity.IDENTITY);
+    }
+
+    public String getNamespace() {
+        return getContentValues().getAsString(Identity.NAMESPACE);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/ImDataItem.java b/src/com/android/contacts/common/model/dataitem/ImDataItem.java
new file mode 100644
index 0000000..532b89f
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/ImDataItem.java
@@ -0,0 +1,83 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+
+/**
+ * Represents an IM data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Im}.
+ */
+public class ImDataItem extends DataItem {
+
+    private final boolean mCreatedFromEmail;
+
+    /* package */ ImDataItem(ContentValues values) {
+        super(values);
+        mCreatedFromEmail = false;
+    }
+
+    private ImDataItem(ContentValues values, boolean createdFromEmail) {
+        super(values);
+        mCreatedFromEmail = createdFromEmail;
+    }
+
+    public static ImDataItem createFromEmail(EmailDataItem item) {
+        ImDataItem im = new ImDataItem(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);
+        }
+    }
+
+    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/common/model/dataitem/NicknameDataItem.java b/src/com/android/contacts/common/model/dataitem/NicknameDataItem.java
new file mode 100644
index 0000000..e7f9d4a
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/NicknameDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+
+/**
+ * Represents a nickname data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Nickname}.
+ */
+public class NicknameDataItem extends DataItem {
+
+    public NicknameDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getName() {
+        return getContentValues().getAsString(Nickname.NAME);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Nickname.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/NoteDataItem.java b/src/com/android/contacts/common/model/dataitem/NoteDataItem.java
new file mode 100644
index 0000000..3d71167
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/NoteDataItem.java
@@ -0,0 +1,36 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+
+/**
+ * Represents a note data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Note}.
+ */
+public class NoteDataItem extends DataItem {
+
+    /* package */ NoteDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getNote() {
+        return getContentValues().getAsString(Note.NOTE);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java b/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
new file mode 100644
index 0000000..9f4b8d3
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
@@ -0,0 +1,68 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+
+/**
+ * Represents an organization data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Organization}.
+ */
+public class OrganizationDataItem extends DataItem {
+
+    /* package */ OrganizationDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getCompany() {
+        return getContentValues().getAsString(Organization.COMPANY);
+    }
+
+    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/common/model/dataitem/PhoneDataItem.java b/src/com/android/contacts/common/model/dataitem/PhoneDataItem.java
new file mode 100644
index 0000000..f45e025
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/PhoneDataItem.java
@@ -0,0 +1,80 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.telephony.PhoneNumberUtils;
+
+import com.android.contacts.common.model.dataitem.DataKind;
+
+/**
+ * Represents a phone data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Phone}.
+ */
+public class PhoneDataItem extends DataItem {
+
+    public static final String KEY_FORMATTED_PHONE_NUMBER = "formattedPhoneNumber";
+
+    /* package */ PhoneDataItem(ContentValues values) {
+        super(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);
+    }
+
+    public String getFormattedPhoneNumber() {
+        return getContentValues().getAsString(KEY_FORMATTED_PHONE_NUMBER);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Phone.LABEL);
+    }
+
+    public void computeFormattedPhoneNumber(String defaultCountryIso) {
+        final String phoneNumber = getNumber();
+        if (phoneNumber != null) {
+            final String formattedPhoneNumber = PhoneNumberUtils.formatNumber(phoneNumber,
+                    getNormalizedNumber(), defaultCountryIso);
+            getContentValues().put(KEY_FORMATTED_PHONE_NUMBER, formattedPhoneNumber);
+        }
+    }
+
+    /**
+     * Returns the formatted phone number (if already computed using {@link
+     * #computeFormattedPhoneNumber}). Otherwise this method returns the unformatted phone number.
+     */
+    @Override
+    public String buildDataStringForDisplay(Context context, DataKind kind) {
+        final String formatted = getFormattedPhoneNumber();
+        if (formatted != null) {
+            return formatted;
+        } else {
+            return getNumber();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java b/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java
new file mode 100644
index 0000000..a61218b
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts.Photo;
+
+/**
+ * Represents a photo data item, wrapping the columns in
+ * {@link ContactsContract.Contacts.Photo}.
+ */
+public class PhotoDataItem extends DataItem {
+
+    /* package */ PhotoDataItem(ContentValues values) {
+        super(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/common/model/dataitem/RelationDataItem.java b/src/com/android/contacts/common/model/dataitem/RelationDataItem.java
new file mode 100644
index 0000000..b699297
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/RelationDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+
+/**
+ * Represents a relation data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Relation}.
+ */
+public class RelationDataItem extends DataItem {
+
+    /* package */ RelationDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getName() {
+        return getContentValues().getAsString(Relation.NAME);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Relation.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java b/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
new file mode 100644
index 0000000..ec704fc
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+
+/**
+ * Represents a sip address data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.SipAddress}.
+ */
+public class SipAddressDataItem extends DataItem {
+
+    /* package */ SipAddressDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getSipAddress() {
+        return getContentValues().getAsString(SipAddress.SIP_ADDRESS);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(SipAddress.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java b/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
new file mode 100644
index 0000000..ce2c84a
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
@@ -0,0 +1,98 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+
+/**
+ * Represents a structured name data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.StructuredName}.
+ */
+public class StructuredNameDataItem extends DataItem {
+
+    public StructuredNameDataItem() {
+        super(new ContentValues());
+        getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+    }
+
+    /* package */ StructuredNameDataItem(ContentValues values) {
+        super(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/common/model/dataitem/StructuredPostalDataItem.java b/src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
new file mode 100644
index 0000000..6cfc0c1
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
@@ -0,0 +1,68 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+
+/**
+ * Represents a structured postal data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.StructuredPostal}.
+ */
+public class StructuredPostalDataItem extends DataItem {
+
+    /* package */ StructuredPostalDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getFormattedAddress() {
+        return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS);
+    }
+
+    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/common/model/dataitem/WebsiteDataItem.java b/src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
new file mode 100644
index 0000000..0939421
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+/**
+ * Represents a website data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Website}.
+ */
+public class WebsiteDataItem extends DataItem {
+
+    /* package */ WebsiteDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getUrl() {
+        return getContentValues().getAsString(Website.URL);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Website.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/test/InjectedServices.java b/src/com/android/contacts/common/test/InjectedServices.java
new file mode 100644
index 0000000..75ad938
--- /dev/null
+++ b/src/com/android/contacts/common/test/InjectedServices.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.common.test;
+
+import android.content.ContentResolver;
+import android.content.SharedPreferences;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+
+import java.util.HashMap;
+
+/**
+ * A mechanism for providing alternative (mock) services to the application
+ * while running tests. Activities, Services and the Application should check
+ * with this class to see if a particular service has been overridden.
+ */
+public class InjectedServices {
+
+    private ContentResolver mContentResolver;
+    private SharedPreferences mSharedPreferences;
+    private HashMap<String, Object> mSystemServices;
+
+    @VisibleForTesting
+    public void setContentResolver(ContentResolver contentResolver) {
+        this.mContentResolver = contentResolver;
+    }
+
+    public ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    @VisibleForTesting
+    public void setSharedPreferences(SharedPreferences sharedPreferences) {
+        this.mSharedPreferences = sharedPreferences;
+    }
+
+    public SharedPreferences getSharedPreferences() {
+        return mSharedPreferences;
+    }
+
+    @VisibleForTesting
+    public void setSystemService(String name, Object service) {
+        if (mSystemServices == null) {
+            mSystemServices = Maps.newHashMap();
+        }
+
+        mSystemServices.put(name, service);
+    }
+
+    public Object getSystemService(String name) {
+        if (mSystemServices != null) {
+            return mSystemServices.get(name);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/common/util/CommonDateUtils.java b/src/com/android/contacts/common/util/CommonDateUtils.java
index 5dfd149..bba910a 100644
--- a/src/com/android/contacts/common/util/CommonDateUtils.java
+++ b/src/com/android/contacts/common/util/CommonDateUtils.java
@@ -33,4 +33,9 @@
             new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
     public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT =
             new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+
+    /**
+     * Exchange requires 8:00 for birthdays
+     */
+    public final static int DEFAULT_HOUR = 8;
 }
diff --git a/src/com/android/contacts/common/util/ContactLoaderUtils.java b/src/com/android/contacts/common/util/ContactLoaderUtils.java
new file mode 100644
index 0000000..0ec8887
--- /dev/null
+++ b/src/com/android/contacts/common/util/ContactLoaderUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 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.common.util;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.net.Uri;
+import android.provider.Contacts;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+
+/**
+ * Utility methods for the {@link ContactLoader}.
+ */
+public final class ContactLoaderUtils {
+
+    /** Static helper, not instantiable. */
+    private ContactLoaderUtils() {}
+
+    /**
+     * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+     * For legacy contacts, a raw-contact lookup is performed. An {@link IllegalArgumentException}
+     * can be thrown if the URI is null or the authority is not recognized.
+     *
+     * Do not call from the UI thread.
+     */
+    @SuppressWarnings("deprecation")
+    public static Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri)
+            throws IllegalArgumentException {
+        if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+        final String authority = uri.getAuthority();
+
+        // Current Style Uri?
+        if (ContactsContract.AUTHORITY.equals(authority)) {
+            final String type = resolver.getType(uri);
+            // Contact-Uri? Good, return it
+            if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+                return uri;
+            }
+
+            // RawContact-Uri? Transform it to ContactUri
+            if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+                final long rawContactId = ContentUris.parseId(uri);
+                return RawContacts.getContactLookupUri(resolver,
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+            }
+
+            // Anything else? We don't know what this is
+            throw new IllegalArgumentException("uri format is unknown");
+        }
+
+        // Legacy Style? Convert to RawContact
+        final String OBSOLETE_AUTHORITY = Contacts.AUTHORITY;
+        if (OBSOLETE_AUTHORITY.equals(authority)) {
+            // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+            final long rawContactId = ContentUris.parseId(uri);
+            return RawContacts.getContactLookupUri(resolver,
+                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+        }
+
+        throw new IllegalArgumentException("uri authority is unknown");
+    }
+}
diff --git a/src/com/android/contacts/common/util/DataStatus.java b/src/com/android/contacts/common/util/DataStatus.java
new file mode 100644
index 0000000..76f11b6
--- /dev/null
+++ b/src/com/android/contacts/common/util/DataStatus.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.Data;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import com.android.contacts.common.R;
+
+/**
+ * Storage for a social status update. Holds a single update, but can use
+ * {@link #possibleUpdate(Cursor)} to consider updating when a better status
+ * exists. Statuses with timestamps, or with newer timestamps win.
+ */
+public class DataStatus {
+    private int mPresence = -1;
+    private String mStatus = null;
+    private long mTimestamp = -1;
+
+    private String mResPackage = null;
+    private int mIconRes = -1;
+    private int mLabelRes = -1;
+
+    public DataStatus() {
+    }
+
+    public DataStatus(Cursor cursor) {
+        // When creating from cursor row, fill normally
+        fromCursor(cursor);
+    }
+
+    /**
+     * Attempt updating this {@link DataStatus} based on values at the
+     * current row of the given {@link Cursor}.
+     */
+    public void possibleUpdate(Cursor cursor) {
+        final boolean hasStatus = !isNull(cursor, Data.STATUS);
+        final boolean hasTimestamp = !isNull(cursor, Data.STATUS_TIMESTAMP);
+
+        // Bail early when not valid status, or when previous status was
+        // found and we can't compare this one.
+        if (!hasStatus) return;
+        if (isValid() && !hasTimestamp) return;
+
+        if (hasTimestamp) {
+            // Compare timestamps and bail if older status
+            final long newTimestamp = getLong(cursor, Data.STATUS_TIMESTAMP, -1);
+            if (newTimestamp < mTimestamp) return;
+
+            mTimestamp = newTimestamp;
+        }
+
+        // Fill in remaining details from cursor
+        fromCursor(cursor);
+    }
+
+    private void fromCursor(Cursor cursor) {
+        mPresence = getInt(cursor, Data.PRESENCE, -1);
+        mStatus = getString(cursor, Data.STATUS);
+        mTimestamp = getLong(cursor, Data.STATUS_TIMESTAMP, -1);
+        mResPackage = getString(cursor, Data.STATUS_RES_PACKAGE);
+        mIconRes = getInt(cursor, Data.STATUS_ICON, -1);
+        mLabelRes = getInt(cursor, Data.STATUS_LABEL, -1);
+    }
+
+    public boolean isValid() {
+        return !TextUtils.isEmpty(mStatus);
+    }
+
+    public int getPresence() {
+        return mPresence;
+    }
+
+    public CharSequence getStatus() {
+        return mStatus;
+    }
+
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
+    /**
+     * Build any timestamp and label into a single string.
+     */
+    public CharSequence getTimestampLabel(Context context) {
+        final PackageManager pm = context.getPackageManager();
+
+        // Use local package for resources when none requested
+        if (mResPackage == null) mResPackage = context.getPackageName();
+
+        final boolean validTimestamp = mTimestamp > 0;
+        final boolean validLabel = mResPackage != null && mLabelRes != -1;
+
+        final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
+                mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
+                DateUtils.FORMAT_ABBREV_RELATIVE) : null;
+        final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
+                null) : null;
+
+        if (validTimestamp && validLabel) {
+            return context.getString(
+                    R.string.contact_status_update_attribution_with_date,
+                    timeClause, labelClause);
+        } else if (validLabel) {
+            return context.getString(
+                    R.string.contact_status_update_attribution,
+                    labelClause);
+        } else if (validTimestamp) {
+            return timeClause;
+        } else {
+            return null;
+        }
+    }
+
+    public Drawable getIcon(Context context) {
+        final PackageManager pm = context.getPackageManager();
+
+        // Use local package for resources when none requested
+        if (mResPackage == null) mResPackage = context.getPackageName();
+
+        final boolean validIcon = mResPackage != null && mIconRes != -1;
+        return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
+    }
+
+    private static String getString(Cursor cursor, String columnName) {
+        return cursor.getString(cursor.getColumnIndex(columnName));
+    }
+
+    private static int getInt(Cursor cursor, String columnName) {
+        return cursor.getInt(cursor.getColumnIndex(columnName));
+    }
+
+    private static int getInt(Cursor cursor, String columnName, int missingValue) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.isNull(columnIndex) ? missingValue : cursor.getInt(columnIndex);
+    }
+
+    private static long getLong(Cursor cursor, String columnName, long missingValue) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.isNull(columnIndex) ? missingValue : cursor.getLong(columnIndex);
+    }
+
+    private static boolean isNull(Cursor cursor, String columnName) {
+        return cursor.isNull(cursor.getColumnIndex(columnName));
+    }
+}
diff --git a/src/com/android/contacts/common/util/DateUtils.java b/src/com/android/contacts/common/util/DateUtils.java
new file mode 100644
index 0000000..f527eb9
--- /dev/null
+++ b/src/com/android/contacts/common/util/DateUtils.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2010 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.common.util;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+
+
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Utility methods for processing dates.
+ */
+public class DateUtils {
+    public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
+
+    /**
+     * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year.
+     * Let's add a one-off hack for that day of the year
+     */
+    public static final String NO_YEAR_DATE_FEB29TH = "--02-29";
+
+    // Variations of ISO 8601 date format.  Do not change the order - it does affect the
+    // result in ambiguous cases.
+    private static final SimpleDateFormat[] DATE_FORMATS = {
+        CommonDateUtils.FULL_DATE_FORMAT,
+        CommonDateUtils.DATE_AND_TIME_FORMAT,
+        new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US),
+        new SimpleDateFormat("yyyyMMdd", Locale.US),
+        new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US),
+        new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US),
+        new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US),
+    };
+
+    static {
+        for (SimpleDateFormat format : DATE_FORMATS) {
+            format.setLenient(true);
+            format.setTimeZone(UTC_TIMEZONE);
+        }
+        CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE);
+    }
+
+    /**
+     * Parses the supplied string to see if it looks like a date.
+     *
+     * @param string The string representation of the provided date
+     * @param mustContainYear If true, the string is parsed as a date containing a year. If false,
+     * the string is parsed into a valid date even if the year field is missing.
+     * @return A Calendar object corresponding to the date if the string is successfully parsed.
+     * If not, null is returned.
+     */
+    public static Calendar parseDate(String string, boolean mustContainYear) {
+        ParsePosition parsePosition = new ParsePosition(0);
+        Date date;
+        if (!mustContainYear) {
+            final boolean noYearParsed;
+            // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately
+            if (NO_YEAR_DATE_FEB29TH.equals(string)) {
+                return getUtcDate(0, Calendar.FEBRUARY, 29);
+            } else {
+                synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) {
+                    date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition);
+                }
+                noYearParsed = parsePosition.getIndex() == string.length();
+            }
+
+            if (noYearParsed) {
+                return getUtcDate(date, true);
+            }
+        }
+        for (int i = 0; i < DATE_FORMATS.length; i++) {
+            SimpleDateFormat f = DATE_FORMATS[i];
+            synchronized (f) {
+                parsePosition.setIndex(0);
+                date = f.parse(string, parsePosition);
+                if (parsePosition.getIndex() == string.length()) {
+                    return getUtcDate(date, false);
+                }
+            }
+        }
+        return null;
+    }
+
+    private static final Calendar getUtcDate(Date date, boolean noYear) {
+        final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+        calendar.setTime(date);
+        if (noYear) {
+            calendar.set(Calendar.YEAR, 0);
+        }
+        return calendar;
+    }
+
+    private static final Calendar getUtcDate(int year, int month, int dayOfMonth) {
+        final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+        calendar.clear();
+        calendar.set(Calendar.YEAR, year);
+        calendar.set(Calendar.MONTH, month);
+        calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+        return calendar;
+    }
+
+    public static boolean isYearSet(Calendar cal) {
+        // use the Calendar.YEAR field to track whether or not the year is set instead of
+        // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become
+        // true irregardless of what the previous value was
+        return cal.get(Calendar.YEAR) > 1;
+    }
+
+    /**
+     * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with
+     * longForm set to {@code true} by default.
+     *
+     * @param context Valid context
+     * @param string String representation of a date to parse
+     * @return Returns the same date in a cleaned up format. If the supplied string does not look
+     * like a date, return it unchanged.
+     */
+
+    public static String formatDate(Context context, String string) {
+        return formatDate(context, string, true);
+    }
+
+    /**
+     * Parses the supplied string to see if it looks like a date.
+     *
+     * @param context Valid context
+     * @param string String representation of a date to parse
+     * @param longForm If true, return the date formatted into its long string representation.
+     * If false, return the date formatted using its short form representation (i.e. 12/11/2012)
+     * @return Returns the same date in a cleaned up format. If the supplied string does not look
+     * like a date, return it unchanged.
+     */
+    public static String formatDate(Context context, String string, boolean longForm) {
+        if (string == null) {
+            return null;
+        }
+
+        string = string.trim();
+        if (string.length() == 0) {
+            return string;
+        }
+        final Calendar cal = parseDate(string, false);
+
+        // we weren't able to parse the string successfully so just return it unchanged
+        if (cal == null) {
+            return string;
+        }
+
+        final boolean isYearSet = isYearSet(cal);
+        final java.text.DateFormat outFormat;
+        if (!isYearSet) {
+            outFormat = getLocalizedDateFormatWithoutYear(context);
+        } else {
+            outFormat =
+                    longForm ? DateFormat.getLongDateFormat(context) :
+                    DateFormat.getDateFormat(context);
+        }
+        synchronized (outFormat) {
+            outFormat.setTimeZone(UTC_TIMEZONE);
+            return outFormat.format(cal.getTime());
+        }
+    }
+
+    public static boolean isMonthBeforeDay(Context context) {
+        char[] dateFormatOrder = DateFormat.getDateFormatOrder(context);
+        for (int i = 0; i < dateFormatOrder.length; i++) {
+            if (dateFormatOrder[i] == DateFormat.DATE) {
+                return false;
+            }
+            if (dateFormatOrder[i] == DateFormat.MONTH) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a SimpleDateFormat object without the year fields by using a regular expression
+     * to eliminate the year in the string pattern. In the rare occurence that the resulting
+     * pattern cannot be reconverted into a SimpleDateFormat, it uses the provided context to
+     * determine whether the month field should be displayed before the day field, and returns
+     * either "MMMM dd" or "dd MMMM" converted into a SimpleDateFormat.
+     */
+    public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) {
+        final String pattern = ((SimpleDateFormat) SimpleDateFormat.getDateInstance(
+                java.text.DateFormat.LONG)).toPattern();
+        // Determine the correct regex pattern for year.
+        // Special case handling for Spanish locale by checking for "de"
+        final String yearPattern = pattern.contains(
+                "de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*";
+        try {
+         // Eliminate the substring in pattern that matches the format for that of year
+            return new SimpleDateFormat(pattern.replaceAll(yearPattern, ""));
+        } catch (IllegalArgumentException e) {
+            return new SimpleDateFormat(
+                    DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM");
+        }
+    }
+
+    /**
+     * Given a calendar (possibly containing only a day of the year), returns the earliest possible
+     * anniversary of the date that is equal to or after the current point in time if the date
+     * does not contain a year, or the date converted to the local time zone (if the date contains
+     * a year.
+     *
+     * @param target The date we wish to convert(in the UTC time zone).
+     * @return If date does not contain a year (year < 1900), returns the next earliest anniversary
+     * that is after the current point in time (in the local time zone). Otherwise, returns the
+     * adjusted Date in the local time zone.
+     */
+    public static Date getNextAnnualDate(Calendar target) {
+        final Calendar today = Calendar.getInstance();
+        today.setTime(new Date());
+
+        // Round the current time to the exact start of today so that when we compare
+        // today against the target date, both dates are set to exactly 0000H.
+        today.set(Calendar.HOUR_OF_DAY, 0);
+        today.set(Calendar.MINUTE, 0);
+        today.set(Calendar.SECOND, 0);
+        today.set(Calendar.MILLISECOND, 0);
+
+        final boolean isYearSet = isYearSet(target);
+        final int targetYear = target.get(Calendar.YEAR);
+        final int targetMonth = target.get(Calendar.MONTH);
+        final int targetDay = target.get(Calendar.DAY_OF_MONTH);
+        final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29);
+        final GregorianCalendar anniversary = new GregorianCalendar();
+        // Convert from the UTC date to the local date. Set the year to today's year if the
+        // there is no provided year (targetYear < 1900)
+        anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear,
+                targetMonth, targetDay);
+        // If the anniversary's date is before the start of today and there is no year set,
+        // increment the year by 1 so that the returned date is always equal to or greater than
+        // today. If the day is a leap year, keep going until we get the next leap year anniversary
+        // Otherwise if there is already a year set, simply return the exact date.
+        if (!isYearSet) {
+            int anniversaryYear = today.get(Calendar.YEAR);
+            if (anniversary.before(today) ||
+                    (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) {
+                // If the target date is not Feb 29, then set the anniversary to the next year.
+                // Otherwise, keep going until we find the next leap year (this is not guaranteed
+                // to be in 4 years time).
+                do {
+                    anniversaryYear +=1;
+                } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear));
+                anniversary.set(anniversaryYear, targetMonth, targetDay);
+            }
+        }
+        return anniversary.getTime();
+    }
+}
diff --git a/src/com/android/contacts/common/util/NameConverter.java b/src/com/android/contacts/common/util/NameConverter.java
new file mode 100644
index 0000000..56f3192
--- /dev/null
+++ b/src/com/android/contacts/common/util/NameConverter.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2011 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.common.util;
+
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Utility class for converting between a display name and structured name (and vice-versa), via
+ * calls to the contact provider.
+ */
+public class NameConverter {
+
+    /**
+     * The array of fields that comprise a structured name.
+     */
+    public static final String[] STRUCTURED_NAME_FIELDS = new String[] {
+            StructuredName.PREFIX,
+            StructuredName.GIVEN_NAME,
+            StructuredName.MIDDLE_NAME,
+            StructuredName.FAMILY_NAME,
+            StructuredName.SUFFIX
+    };
+
+    /**
+     * Converts the given structured name (provided as a map from {@link StructuredName} fields to
+     * corresponding values) into a display name string.
+     * <p>
+     * Note that this operates via a call back to the ContactProvider, but it does not access the
+     * database, so it should be safe to call from the UI thread.  See
+     * ContactsProvider2.completeName() for the underlying method call.
+     * @param context Activity context.
+     * @param structuredName The structured name map to convert.
+     * @return The display name computed from the structured name map.
+     */
+    public static String structuredNameToDisplayName(Context context,
+            Map<String, String> structuredName) {
+        Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+        for (String key : STRUCTURED_NAME_FIELDS) {
+            if (structuredName.containsKey(key)) {
+                appendQueryParameter(builder, key, structuredName.get(key));
+            }
+        }
+        return fetchDisplayName(context, builder.build());
+    }
+
+    /**
+     * Converts the given structured name (provided as ContentValues) into a display name string.
+     * @param context Activity context.
+     * @param values The content values containing values comprising the structured name.
+     * @return
+     */
+    public static String structuredNameToDisplayName(Context context, ContentValues values) {
+        Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+        for (String key : STRUCTURED_NAME_FIELDS) {
+            if (values.containsKey(key)) {
+                appendQueryParameter(builder, key, values.getAsString(key));
+            }
+        }
+        return fetchDisplayName(context, builder.build());
+    }
+
+    /**
+     * Helper method for fetching the display name via the given URI.
+     */
+    private static String fetchDisplayName(Context context, Uri uri) {
+        String displayName = null;
+        Cursor cursor = context.getContentResolver().query(uri, new String[]{
+                StructuredName.DISPLAY_NAME,
+        }, null, null, null);
+
+        try {
+            if (cursor.moveToFirst()) {
+                displayName = cursor.getString(0);
+            }
+        } finally {
+            cursor.close();
+        }
+        return displayName;
+    }
+
+    /**
+     * Converts the given display name string into a structured name (as a map from
+     * {@link StructuredName} fields to corresponding values).
+     * <p>
+     * Note that this operates via a call back to the ContactProvider, but it does not access the
+     * database, so it should be safe to call from the UI thread.
+     * @param context Activity context.
+     * @param displayName The display name to convert.
+     * @return The structured name map computed from the display name.
+     */
+    public static Map<String, String> displayNameToStructuredName(Context context,
+            String displayName) {
+        Map<String, String> structuredName = new TreeMap<String, String>();
+        Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+
+        appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName);
+        Cursor cursor = context.getContentResolver().query(builder.build(), STRUCTURED_NAME_FIELDS,
+                null, null, null);
+
+        try {
+            if (cursor.moveToFirst()) {
+                for (int i = 0; i < STRUCTURED_NAME_FIELDS.length; i++) {
+                    structuredName.put(STRUCTURED_NAME_FIELDS[i], cursor.getString(i));
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+        return structuredName;
+    }
+
+    /**
+     * Converts the given display name string into a structured name (inserting the structured
+     * values into a new or existing ContentValues object).
+     * <p>
+     * Note that this operates via a call back to the ContactProvider, but it does not access the
+     * database, so it should be safe to call from the UI thread.
+     * @param context Activity context.
+     * @param displayName The display name to convert.
+     * @param contentValues The content values object to place the structured name values into.  If
+     *     null, a new one will be created and returned.
+     * @return The ContentValues object containing the structured name fields derived from the
+     *     display name.
+     */
+    public static ContentValues displayNameToStructuredName(Context context, String displayName,
+            ContentValues contentValues) {
+        if (contentValues == null) {
+            contentValues = new ContentValues();
+        }
+        Map<String, String> mapValues = displayNameToStructuredName(context, displayName);
+        for (String key : mapValues.keySet()) {
+            contentValues.put(key, mapValues.get(key));
+        }
+        return contentValues;
+    }
+
+    private static void appendQueryParameter(Builder builder, String field, String value) {
+        if (!TextUtils.isEmpty(value)) {
+            builder.appendQueryParameter(field, value);
+        }
+    }
+
+    /**
+     * Parses phonetic name and returns parsed data (family, middle, given) as ContentValues.
+     * Parsed data should be {@link StructuredName#PHONETIC_FAMILY_NAME},
+     * {@link StructuredName#PHONETIC_MIDDLE_NAME}, and
+     * {@link StructuredName#PHONETIC_GIVEN_NAME}.
+     * If this method cannot parse given phoneticName, null values will be stored.
+     *
+     * @param phoneticName Phonetic name to be parsed
+     * @param values ContentValues to be used for storing data. If null, new instance will be
+     * created.
+     * @return ContentValues with parsed data. Those data can be null.
+     */
+    public static StructuredNameDataItem parsePhoneticName(String phoneticName,
+            StructuredNameDataItem item) {
+        String family = null;
+        String middle = null;
+        String given = null;
+
+        if (!TextUtils.isEmpty(phoneticName)) {
+            String[] strings = phoneticName.split(" ", 3);
+            switch (strings.length) {
+                case 1:
+                    family = strings[0];
+                    break;
+                case 2:
+                    family = strings[0];
+                    given = strings[1];
+                    break;
+                case 3:
+                    family = strings[0];
+                    middle = strings[1];
+                    given = strings[2];
+                    break;
+            }
+        }
+
+        if (item == null) {
+            item = new StructuredNameDataItem();
+        }
+        item.setPhoneticFamilyName(family);
+        item.setPhoneticMiddleName(middle);
+        item.setPhoneticGivenName(given);
+        return item;
+    }
+
+    /**
+     * Constructs and returns a phonetic full name from given parts.
+     */
+    public static String buildPhoneticName(String family, String middle, String given) {
+        if (!TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle)
+                || !TextUtils.isEmpty(given)) {
+            StringBuilder sb = new StringBuilder();
+            if (!TextUtils.isEmpty(family)) {
+                sb.append(family.trim()).append(' ');
+            }
+            if (!TextUtils.isEmpty(middle)) {
+                sb.append(middle.trim()).append(' ');
+            }
+            if (!TextUtils.isEmpty(given)) {
+                sb.append(given.trim()).append(' ');
+            }
+            sb.setLength(sb.length() - 1); // Yank the last space
+            return sb.toString();
+        } else {
+            return null;
+        }
+    }
+}