Modifications to bold contacts' first names.

For lists and headers, this is calculated by using a similar technique as we
use in the list view for highlighting text when the sort order and display
order do not match - we look for the overlap point between the display name
and the alternate display name, and bold the leading or trailing part, based
on the display order.

For the edit view, we convert the display name into a structured name (via an
API extracted from the structured name editor code that calls into the provider
to handle splitting), and then bold the span corresponding to the contact's
given name.

This also fixes an issue with the alternate display order in which we weren't
honoring that preference on the contact details view.

Change-Id: I6af048725541996cb303810a56a1ad79fc276e11
diff --git a/res/layout/contact_detail_header_view.xml b/res/layout/contact_detail_header_view.xml
index 80dcacc..328a5ff 100644
--- a/res/layout/contact_detail_header_view.xml
+++ b/res/layout/contact_detail_header_view.xml
@@ -54,7 +54,6 @@
             android:gravity="bottom"
             android:textSize="@dimen/detail_header_name_text_size"
             android:textColor="@color/detail_header_view_text_color"
-            android:textStyle="bold"
          />
 
         <TextView android:id="@+id/phonetic_name"
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index f4baf3b..4b0bbaa 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -93,6 +93,7 @@
         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;
@@ -131,6 +132,7 @@
             mPhotoId = -1;
             mPhotoUri = null;
             mDisplayName = null;
+            mAltDisplayName = null;
             mPhoneticName = null;
             mStarred = false;
             mPresence = null;
@@ -145,8 +147,9 @@
          */
         private Result(Uri uri, Uri lookupUri, long directoryId, String lookupKey, long id,
                 long nameRawContactId, int displayNameSource, long photoId, String photoUri,
-                String displayName, String phoneticName, boolean starred, Integer presence,
-                String status, Long statusTimestamp, Integer statusLabel, String statusResPackage) {
+                String displayName, String altDisplayName, String phoneticName, boolean starred,
+                Integer presence, String status, Long statusTimestamp, Integer statusLabel,
+                String statusResPackage) {
             mLookupUri = lookupUri;
             mUri = uri;
             mDirectoryId = directoryId;
@@ -159,6 +162,7 @@
             mPhotoId = photoId;
             mPhotoUri = photoUri;
             mDisplayName = displayName;
+            mAltDisplayName = altDisplayName;
             mPhoneticName = phoneticName;
             mStarred = starred;
             mPresence = presence;
@@ -179,6 +183,7 @@
             mPhotoId = from.mPhotoId;
             mPhotoUri = from.mPhotoUri;
             mDisplayName = from.mDisplayName;
+            mAltDisplayName = from.mAltDisplayName;
             mPhoneticName = from.mPhoneticName;
             mStarred = from.mStarred;
             mPresence = from.mPresence;
@@ -257,6 +262,10 @@
             return mDisplayName;
         }
 
+        public String getAltDisplayName() {
+            return mAltDisplayName;
+        }
+
         public String getPhoneticName() {
             return mPhoneticName;
         }
@@ -380,6 +389,7 @@
                 Contacts.DISPLAY_NAME_SOURCE,
                 Contacts.LOOKUP_KEY,
                 Contacts.DISPLAY_NAME,
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
                 Contacts.PHONETIC_NAME,
                 Contacts.PHOTO_ID,
                 Contacts.STARRED,
@@ -447,67 +457,68 @@
         public final static int DISPLAY_NAME_SOURCE = 1;
         public final static int LOOKUP_KEY = 2;
         public final static int DISPLAY_NAME = 3;
-        public final static int PHONETIC_NAME = 4;
-        public final static int PHOTO_ID = 5;
-        public final static int STARRED = 6;
-        public final static int CONTACT_PRESENCE = 7;
-        public final static int CONTACT_STATUS = 8;
-        public final static int CONTACT_STATUS_TIMESTAMP = 9;
-        public final static int CONTACT_STATUS_RES_PACKAGE = 10;
-        public final static int CONTACT_STATUS_LABEL = 11;
-        public final static int CONTACT_ID = 12;
-        public final static int RAW_CONTACT_ID = 13;
+        public final static int ALT_DISPLAY_NAME = 4;
+        public final static int PHONETIC_NAME = 5;
+        public final static int PHOTO_ID = 6;
+        public final static int STARRED = 7;
+        public final static int CONTACT_PRESENCE = 8;
+        public final static int CONTACT_STATUS = 9;
+        public final static int CONTACT_STATUS_TIMESTAMP = 10;
+        public final static int CONTACT_STATUS_RES_PACKAGE = 11;
+        public final static int CONTACT_STATUS_LABEL = 12;
+        public final static int CONTACT_ID = 13;
+        public final static int RAW_CONTACT_ID = 14;
 
-        public final static int ACCOUNT_NAME = 14;
-        public final static int ACCOUNT_TYPE = 15;
-        public final static int DIRTY = 16;
-        public final static int VERSION = 17;
-        public final static int SOURCE_ID = 18;
-        public final static int SYNC1 = 19;
-        public final static int SYNC2 = 20;
-        public final static int SYNC3 = 21;
-        public final static int SYNC4 = 22;
-        public final static int DELETED = 23;
-        public final static int IS_RESTRICTED = 24;
-        public final static int NAME_VERIFIED = 25;
+        public final static int ACCOUNT_NAME = 15;
+        public final static int ACCOUNT_TYPE = 16;
+        public final static int DIRTY = 17;
+        public final static int VERSION = 18;
+        public final static int SOURCE_ID = 19;
+        public final static int SYNC1 = 20;
+        public final static int SYNC2 = 21;
+        public final static int SYNC3 = 22;
+        public final static int SYNC4 = 23;
+        public final static int DELETED = 24;
+        public final static int IS_RESTRICTED = 25;
+        public final static int NAME_VERIFIED = 26;
 
-        public final static int DATA_ID = 26;
-        public final static int DATA1 = 27;
-        public final static int DATA2 = 28;
-        public final static int DATA3 = 29;
-        public final static int DATA4 = 30;
-        public final static int DATA5 = 31;
-        public final static int DATA6 = 32;
-        public final static int DATA7 = 33;
-        public final static int DATA8 = 34;
-        public final static int DATA9 = 35;
-        public final static int DATA10 = 36;
-        public final static int DATA11 = 37;
-        public final static int DATA12 = 38;
-        public final static int DATA13 = 39;
-        public final static int DATA14 = 40;
-        public final static int DATA15 = 41;
-        public final static int DATA_SYNC1 = 42;
-        public final static int DATA_SYNC2 = 43;
-        public final static int DATA_SYNC3 = 44;
-        public final static int DATA_SYNC4 = 45;
-        public final static int DATA_VERSION = 46;
-        public final static int IS_PRIMARY = 47;
-        public final static int IS_SUPERPRIMARY = 48;
-        public final static int MIMETYPE = 49;
-        public final static int RES_PACKAGE = 50;
+        public final static int DATA_ID = 27;
+        public final static int DATA1 = 28;
+        public final static int DATA2 = 29;
+        public final static int DATA3 = 30;
+        public final static int DATA4 = 31;
+        public final static int DATA5 = 32;
+        public final static int DATA6 = 33;
+        public final static int DATA7 = 34;
+        public final static int DATA8 = 35;
+        public final static int DATA9 = 36;
+        public final static int DATA10 = 37;
+        public final static int DATA11 = 38;
+        public final static int DATA12 = 39;
+        public final static int DATA13 = 40;
+        public final static int DATA14 = 41;
+        public final static int DATA15 = 42;
+        public final static int DATA_SYNC1 = 43;
+        public final static int DATA_SYNC2 = 44;
+        public final static int DATA_SYNC3 = 45;
+        public final static int DATA_SYNC4 = 46;
+        public final static int DATA_VERSION = 47;
+        public final static int IS_PRIMARY = 48;
+        public final static int IS_SUPERPRIMARY = 49;
+        public final static int MIMETYPE = 50;
+        public final static int RES_PACKAGE = 51;
 
-        public final static int GROUP_SOURCE_ID = 51;
+        public final static int GROUP_SOURCE_ID = 52;
 
-        public final static int PRESENCE = 52;
-        public final static int CHAT_CAPABILITY = 53;
-        public final static int STATUS = 54;
-        public final static int STATUS_RES_PACKAGE = 55;
-        public final static int STATUS_ICON = 56;
-        public final static int STATUS_LABEL = 57;
-        public final static int STATUS_TIMESTAMP = 58;
+        public final static int PRESENCE = 53;
+        public final static int CHAT_CAPABILITY = 54;
+        public final static int STATUS = 55;
+        public final static int STATUS_RES_PACKAGE = 56;
+        public final static int STATUS_ICON = 57;
+        public final static int STATUS_LABEL = 58;
+        public final static int STATUS_TIMESTAMP = 59;
 
-        public final static int PHOTO_URI = 59;
+        public final static int PHOTO_URI = 60;
     }
 
     private static class DirectoryQuery {
@@ -699,6 +710,7 @@
             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);
@@ -726,8 +738,8 @@
 
             return new Result(contactUri, lookupUri, directoryId, lookupKey, contactId,
                     nameRawContactId, displayNameSource, photoId, photoUri, displayName,
-                    phoneticName, starred, presence, status, statusTimestamp, statusLabel,
-                    statusResPackage);
+                    altDisplayName, phoneticName, starred, presence, status, statusTimestamp,
+                    statusLabel, statusResPackage);
         }
 
         /**
diff --git a/src/com/android/contacts/detail/ContactDetailHeaderView.java b/src/com/android/contacts/detail/ContactDetailHeaderView.java
index 4b211b6..b88b43e 100644
--- a/src/com/android/contacts/detail/ContactDetailHeaderView.java
+++ b/src/com/android/contacts/detail/ContactDetailHeaderView.java
@@ -20,6 +20,8 @@
 import com.android.contacts.ContactLoader.Result;
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.R;
+import com.android.contacts.format.FormatUtils;
+import com.android.contacts.preference.ContactsPreferences;
 import com.android.contacts.util.ContactBadgeUtil;
 
 import android.content.ClipData;
@@ -31,10 +33,13 @@
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.Typeface;
 import android.net.Uri;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.DisplayNameSources;
+import android.text.Spanned;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -122,7 +127,8 @@
     public void loadData(ContactLoader.Result contactData) {
         mContactUri = contactData.getLookupUri();
 
-        setDisplayName(contactData.getDisplayName(), contactData.getPhoneticName());
+        setDisplayName(contactData.getDisplayName(), contactData.getAltDisplayName(),
+                contactData.getPhoneticName());
         setCompany(contactData);
         if (contactData.isLoadingPhoto()) {
             setPhoto(null, false);
@@ -188,8 +194,36 @@
     /**
      * Set the display name and phonetic name to show in the header.
      */
-    private void setDisplayName(CharSequence displayName, CharSequence phoneticName) {
-        mDisplayNameView.setText(displayName);
+    private void setDisplayName(CharSequence displayName, CharSequence altDisplayName,
+            CharSequence phoneticName) {
+
+        // Check the preference for display name ordering, and bold the contact's first name if
+        // possible.
+        ContactsPreferences prefs = new ContactsPreferences(getContext());
+        CharSequence styledName;
+        if (prefs.getDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
+            int overlapPoint = FormatUtils.overlapPoint(
+                    displayName.toString(), altDisplayName.toString());
+            if (overlapPoint > 0) {
+                styledName = FormatUtils.applyStyleToSpan(Typeface.BOLD,
+                        displayName, 0, overlapPoint, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+            } else {
+                styledName = displayName;
+            }
+        } else {
+            // Displaying alternate display name.
+            int overlapPoint = FormatUtils.overlapPoint(
+                    altDisplayName.toString(), displayName.toString());
+            if (overlapPoint > 0) {
+                styledName = FormatUtils.applyStyleToSpan(Typeface.BOLD,
+                        altDisplayName, overlapPoint, altDisplayName.length(),
+                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+            } else {
+                styledName = altDisplayName;
+            }
+        }
+        mDisplayNameView.setText(styledName);
+
         if (TextUtils.isEmpty(phoneticName)) {
             mPhoneticNameView.setVisibility(View.GONE);
         } else {
diff --git a/src/com/android/contacts/editor/StructuredNameEditorView.java b/src/com/android/contacts/editor/StructuredNameEditorView.java
index 6daf7b4..5a2ffd3 100644
--- a/src/com/android/contacts/editor/StructuredNameEditorView.java
+++ b/src/com/android/contacts/editor/StructuredNameEditorView.java
@@ -19,19 +19,20 @@
 import com.android.contacts.model.DataKind;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.util.NameConverter;
 
 import android.content.ContentValues;
 import android.content.Context;
-import android.database.Cursor;
 import android.net.Uri;
-import android.net.Uri.Builder;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * A dedicated editor for structured name.  When the user collapses/expands
  * the structured name, it will reparse or recompose the name, but only
@@ -107,29 +108,20 @@
         ValuesDelta values = getValues();
 
         if (!mChanged) {
-            values.put(StructuredName.PREFIX,
-                    mSnapshot.getAsString(StructuredName.PREFIX));
-            values.put(StructuredName.GIVEN_NAME,
-                    mSnapshot.getAsString(StructuredName.GIVEN_NAME));
-            values.put(StructuredName.MIDDLE_NAME,
-                    mSnapshot.getAsString(StructuredName.MIDDLE_NAME));
-            values.put(StructuredName.FAMILY_NAME,
-                    mSnapshot.getAsString(StructuredName.FAMILY_NAME));
-            values.put(StructuredName.SUFFIX,
-                    mSnapshot.getAsString(StructuredName.SUFFIX));
+            for (String field : NameConverter.STRUCTURED_NAME_FIELDS) {
+                values.put(field, mSnapshot.getAsString(field));
+            }
             return;
         }
 
         String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
-        ContentValues tmpCVs = buildStructuredNameFromFullName(
-                getContext(), displayName, null);
-        if (tmpCVs.size() > 0) {
+        Map<String, String> structuredNameMap = NameConverter.displayNameToStructuredName(
+                getContext(), displayName);
+        if (!structuredNameMap.isEmpty()) {
             eraseFullName(values);
-            values.put(StructuredName.PREFIX, tmpCVs.getAsString(StructuredName.PREFIX));
-            values.put(StructuredName.GIVEN_NAME, tmpCVs.getAsString(StructuredName.GIVEN_NAME));
-            values.put(StructuredName.MIDDLE_NAME, tmpCVs.getAsString(StructuredName.MIDDLE_NAME));
-            values.put(StructuredName.FAMILY_NAME, tmpCVs.getAsString(StructuredName.FAMILY_NAME));
-            values.put(StructuredName.SUFFIX, tmpCVs.getAsString(StructuredName.SUFFIX));
+            for (String field : structuredNameMap.keySet()) {
+                values.put(field, structuredNameMap.get(field));
+            }
         }
 
         mSnapshot.clear();
@@ -137,37 +129,6 @@
         mSnapshot.put(StructuredName.DISPLAY_NAME, displayName);
     }
 
-    public static ContentValues buildStructuredNameFromFullName(
-            Context context, String displayName, ContentValues contentValues) {
-        if (contentValues == null) {
-            contentValues = new ContentValues();
-        }
-
-        Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
-        appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName);
-        Cursor cursor = context.getContentResolver().query(builder.build(), new String[]{
-                StructuredName.PREFIX,
-                StructuredName.GIVEN_NAME,
-                StructuredName.MIDDLE_NAME,
-                StructuredName.FAMILY_NAME,
-                StructuredName.SUFFIX,
-        }, null, null, null);
-
-        try {
-            if (cursor.moveToFirst()) {
-                contentValues.put(StructuredName.PREFIX, cursor.getString(0));
-                contentValues.put(StructuredName.GIVEN_NAME, cursor.getString(1));
-                contentValues.put(StructuredName.MIDDLE_NAME, cursor.getString(2));
-                contentValues.put(StructuredName.FAMILY_NAME, cursor.getString(3));
-                contentValues.put(StructuredName.SUFFIX, cursor.getString(4));
-            }
-        } finally {
-            cursor.close();
-        }
-
-        return contentValues;
-    }
-
     private void switchFromStructuredNameToFullName() {
         ValuesDelta values = getValues();
 
@@ -177,14 +138,9 @@
             return;
         }
 
-        String prefix = values.getAsString(StructuredName.PREFIX);
-        String givenName = values.getAsString(StructuredName.GIVEN_NAME);
-        String middleName = values.getAsString(StructuredName.MIDDLE_NAME);
-        String familyName = values.getAsString(StructuredName.FAMILY_NAME);
-        String suffix = values.getAsString(StructuredName.SUFFIX);
-
-        String displayName = buildFullNameFromStructuredName(getContext(),
-                prefix, givenName, middleName, familyName, suffix);
+        Map<String, String> structuredNameMap = valuesToStructuredNameMap(values);
+        String displayName = NameConverter.structuredNameToDisplayName(getContext(),
+                structuredNameMap);
         if (!TextUtils.isEmpty(displayName)) {
             eraseStructuredName(values);
             values.put(StructuredName.DISPLAY_NAME, displayName);
@@ -192,35 +148,17 @@
 
         mSnapshot.clear();
         mSnapshot.put(StructuredName.DISPLAY_NAME, values.getAsString(StructuredName.DISPLAY_NAME));
-        mSnapshot.put(StructuredName.PREFIX, prefix);
-        mSnapshot.put(StructuredName.GIVEN_NAME, givenName);
-        mSnapshot.put(StructuredName.MIDDLE_NAME, middleName);
-        mSnapshot.put(StructuredName.FAMILY_NAME, familyName);
-        mSnapshot.put(StructuredName.SUFFIX, suffix);
+        for (String field : structuredNameMap.keySet()) {
+            mSnapshot.put(field, structuredNameMap.get(field));
+        }
     }
 
-    public static String buildFullNameFromStructuredName(Context context,
-            String prefix, String given, String middle, String family, String suffix) {
-        Uri.Builder builder = ContactsContract.AUTHORITY_URI.buildUpon()
-                .appendPath("complete_name");
-        appendQueryParameter(builder, StructuredName.PREFIX, prefix);
-        appendQueryParameter(builder, StructuredName.GIVEN_NAME, given);
-        appendQueryParameter(builder, StructuredName.MIDDLE_NAME, middle);
-        appendQueryParameter(builder, StructuredName.FAMILY_NAME, family);
-        appendQueryParameter(builder, StructuredName.SUFFIX, suffix);
-        Cursor cursor = context.getContentResolver().query(builder.build(), new String[]{
-                StructuredName.DISPLAY_NAME,
-        }, null, null, null);
-
-        try {
-            if (cursor.moveToFirst()) {
-                return cursor.getString(0);
-            }
-        } finally {
-            cursor.close();
+    private Map<String, String> valuesToStructuredNameMap(ValuesDelta values) {
+        Map<String, String> structuredNameMap = new HashMap<String, String>();
+        for (String key : NameConverter.STRUCTURED_NAME_FIELDS) {
+            structuredNameMap.put(key, values.getAsString(key));
         }
-
-        return null;
+        return structuredNameMap;
     }
 
     private void eraseFullName(ValuesDelta values) {
@@ -228,11 +166,9 @@
     }
 
     private void eraseStructuredName(ValuesDelta values) {
-        values.putNull(StructuredName.PREFIX);
-        values.putNull(StructuredName.GIVEN_NAME);
-        values.putNull(StructuredName.MIDDLE_NAME);
-        values.putNull(StructuredName.FAMILY_NAME);
-        values.putNull(StructuredName.SUFFIX);
+        for (String field : NameConverter.STRUCTURED_NAME_FIELDS) {
+            values.putNull(field);
+        }
     }
 
     private static void appendQueryParameter(Uri.Builder builder, String field, String value) {
diff --git a/src/com/android/contacts/editor/TextFieldsEditorView.java b/src/com/android/contacts/editor/TextFieldsEditorView.java
index e74492c..04a9485 100644
--- a/src/com/android/contacts/editor/TextFieldsEditorView.java
+++ b/src/com/android/contacts/editor/TextFieldsEditorView.java
@@ -22,19 +22,22 @@
 import com.android.contacts.model.DataKind;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.util.NameConverter;
 
 import android.content.Context;
 import android.content.Entity;
 import android.graphics.Rect;
+import android.graphics.Typeface;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.text.Editable;
 import android.text.InputType;
+import android.text.Spannable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
+import android.text.style.StyleSpan;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.View;
@@ -42,7 +45,8 @@
 import android.widget.EditText;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
-import android.widget.Toast;
+
+import java.util.Map;
 
 /**
  * Simple editor that handles labels and any {@link EditField} defined for the
@@ -189,6 +193,9 @@
             }
             int inputType = field.inputType;
             fieldView.setInputType(inputType);
+            if (field.isFullName) {
+                fieldView.addTextChangedListener(new NameFormattingTextWatcher());
+            }
             if (inputType == InputType.TYPE_CLASS_PHONE) {
                 fieldView.addTextChangedListener(new PhoneNumberFormattingTextWatcher(
                         ContactsUtils.getCurrentCountryIso(mContext)));
@@ -351,4 +358,60 @@
             }
         };
     }
+
+    private class NameFormattingTextWatcher implements TextWatcher {
+
+
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+        @Override
+        public void afterTextChanged(Editable s) {
+            String displayName = s.toString();
+            Map<String, String> structuredName = NameConverter.displayNameToStructuredName(
+                    getContext(), displayName);
+            String givenName = structuredName.get(StructuredName.GIVEN_NAME);
+            if (!TextUtils.isEmpty(givenName)) {
+                int spanStart = -1;
+                int spanEnd = -1;
+                if (displayName.startsWith(givenName + " ")) {
+                    spanStart = 0;
+                    spanEnd = givenName.length();
+                } else {
+                    spanStart = displayName.lastIndexOf(" " + givenName);
+                    if (spanStart > -1) {
+                        spanStart++;
+                        spanEnd = spanStart + givenName.length();
+                    }
+                }
+
+                // If the requested range is already bolded, don't make any changes.
+                if (spanStart > -1) {
+                    StyleSpan[] existingSpans = s.getSpans(0, s.length(), StyleSpan.class);
+                    for (StyleSpan span : existingSpans) {
+                        if (span.getStyle() == Typeface.BOLD
+                                && s.getSpanStart(span.getUnderlying()) == spanStart
+                                && s.getSpanEnd(span.getUnderlying()) == spanEnd) {
+                            // Nothing to do - the correct portion is already bolded.
+                            return;
+                        }
+                    }
+
+                    // Clear any existing bold style spans.
+                    for (StyleSpan span : existingSpans) {
+                        if (span.getStyle() == Typeface.BOLD) {
+                            s.removeSpan(span);
+                        }
+                    }
+
+                    // Set the new bold span.
+                    s.setSpan(new StyleSpan(Typeface.BOLD), spanStart, spanEnd,
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                }
+            }
+        }
+    }
 }
diff --git a/src/com/android/contacts/format/FormatUtils.java b/src/com/android/contacts/format/FormatUtils.java
new file mode 100644
index 0000000..ac34d6d
--- /dev/null
+++ b/src/com/android/contacts/format/FormatUtils.java
@@ -0,0 +1,108 @@
+/*
+ * 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.format;
+
+import android.database.CharArrayBuffer;
+import android.graphics.Typeface;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+
+import java.util.Arrays;
+
+/**
+ * Assorted utility methods related to text formatting in Contacts.
+ */
+public class FormatUtils {
+
+    /**
+     * Finds the earliest point in buffer1 at which the first part of buffer2 matches.  For example,
+     * overlapPoint("abcd", "cdef") == 2.
+     */
+    public static int overlapPoint(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
+        if (buffer1 == null || buffer2 == null) {
+            return -1;
+        }
+        return overlapPoint(Arrays.copyOfRange(buffer1.data, 0, buffer1.sizeCopied),
+                Arrays.copyOfRange(buffer2.data, 0, buffer2.sizeCopied));
+    }
+
+    /**
+     * Finds the earliest point in string1 at which the first part of string2 matches.  For example,
+     * overlapPoint("abcd", "cdef") == 2.
+     */
+    public static int overlapPoint(String string1, String string2) {
+        if (string1 == null || string2 == null) {
+            return -1;
+        }
+        return overlapPoint(string1.toCharArray(), string2.toCharArray());
+    }
+
+    /**
+     * Finds the earliest point in array1 at which the first part of array2 matches.  For example,
+     * overlapPoint("abcd", "cdef") == 2.
+     */
+    public static int overlapPoint(char[] array1, char[] array2) {
+        if (array1 == null || array2 == null) {
+            return -1;
+        }
+        int count1 = array1.length;
+        int count2 = array2.length;
+
+        // Ignore matching tails of the two arrays.
+        while (count1 > 0 && count2 > 0 && array1[count1 - 1] == array2[count2 - 1]) {
+            count1--;
+            count2--;
+        }
+
+        int size = count2;
+        for (int i = 0; i < count1; i++) {
+            if (i + size > count1) {
+                size = count1 - i;
+            }
+            int j;
+            for (j = 0; j < size; j++) {
+                if (array1[i+j] != array2[j]) {
+                    break;
+                }
+            }
+            if (j == size) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Applies the given style to a range of the input CharSequence.
+     * @param style The style to apply (see the style constants in {@link Typeface}).
+     * @param input The CharSequence to style.
+     * @param start Starting index of the range to style (will be clamped to be a minimum of 0).
+     * @param end Ending index of the range to style (will be clamped to a maximum of the input
+     *     length).
+     * @param flags Bitmask for configuring behavior of the span.  See {@link android.text.Spanned}.
+     * @return The styled CharSequence.
+     */
+    public static CharSequence applyStyleToSpan(int style, CharSequence input, int start, int end,
+            int flags) {
+        // Enforce bounds of the char sequence.
+        start = Math.max(0, start);
+        end = Math.min(input.length(), end);
+        SpannableString text = new SpannableString(input);
+        text.setSpan(new StyleSpan(style), start, end, flags);
+        return text;
+    }
+}
diff --git a/src/com/android/contacts/list/ContactListAdapter.java b/src/com/android/contacts/list/ContactListAdapter.java
index cfacc85..2c57983 100644
--- a/src/com/android/contacts/list/ContactListAdapter.java
+++ b/src/com/android/contacts/list/ContactListAdapter.java
@@ -274,8 +274,8 @@
     }
 
     protected void bindName(final ContactListItemView view, Cursor cursor) {
-        view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
-                mAlternativeDisplayNameColumnIndex);
+        view.showDisplayName(cursor, mDisplayNameColumnIndex, mAlternativeDisplayNameColumnIndex,
+                isNameHighlightingEnabled(), getContactNameDisplayOrder());
         view.showPhoneticName(cursor, CONTACT_PHONETIC_NAME_COLUMN_INDEX);
     }
 
diff --git a/src/com/android/contacts/list/ContactListItemView.java b/src/com/android/contacts/list/ContactListItemView.java
index fe5f8e2..022c2f6 100644
--- a/src/com/android/contacts/list/ContactListItemView.java
+++ b/src/com/android/contacts/list/ContactListItemView.java
@@ -18,6 +18,7 @@
 
 import com.android.contacts.ContactPresenceIconUtil;
 import com.android.contacts.R;
+import com.android.contacts.format.FormatUtils;
 import com.android.contacts.widget.TextWithHighlighting;
 import com.android.contacts.widget.TextWithHighlightingFactory;
 
@@ -30,8 +31,11 @@
 import android.graphics.Rect;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
+import android.text.Spannable;
 import android.text.SpannableString;
+import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
 import android.text.style.ForegroundColorSpan;
@@ -45,6 +49,8 @@
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
 
+import java.util.Arrays;
+
 /**
  * A custom view for an item in the contact list.
  */
@@ -805,9 +811,21 @@
         return mActivatedStateSupported ? TruncateAt.START : TruncateAt.MARQUEE;
     }
 
-    public void showDisplayName(Cursor cursor, int nameColumnIndex, boolean highlightingEnabled,
-            int alternativeNameColumnIndex) {
+    public void showDisplayName(Cursor cursor, int nameColumnIndex, int alternativeNameColumnIndex,
+            boolean highlightingEnabled, int displayOrder) {
+
+        // Copy out the display name and alternate display name, and compute the point at which
+        // the two overlap (for bolding).
         cursor.copyStringToBuffer(nameColumnIndex, mNameBuffer);
+        cursor.copyStringToBuffer(alternativeNameColumnIndex, mHighlightedTextBuffer);
+        int overlapPoint = FormatUtils.overlapPoint(mNameBuffer, mHighlightedTextBuffer);
+        int boldStart = 0;
+        int boldEnd = overlapPoint;
+        if (displayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
+            boldStart = overlapPoint;
+            boldEnd = mNameBuffer.sizeCopied;
+        }
+
         TextView nameView = getNameTextView();
         int size = mNameBuffer.sizeCopied;
         if (size != 0) {
@@ -818,11 +836,24 @@
                     mTextWithHighlighting =
                             mTextWithHighlightingFactory.createTextWithHighlighting();
                 }
-                cursor.copyStringToBuffer(alternativeNameColumnIndex, mHighlightedTextBuffer);
                 mTextWithHighlighting.setText(mNameBuffer, mHighlightedTextBuffer);
-                nameView.setText(mTextWithHighlighting);
+                if (overlapPoint > 0) {
+                    // Bold the first name.
+                    nameView.setText(FormatUtils.applyStyleToSpan(Typeface.BOLD,
+                            mTextWithHighlighting, boldStart, boldEnd,
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE));
+                } else {
+                    nameView.setText(mTextWithHighlighting);
+                }
             } else {
-                nameView.setText(mNameBuffer.data, 0, size);
+                if (overlapPoint > 0) {
+                    // Bold the first name.
+                    nameView.setText(FormatUtils.applyStyleToSpan(Typeface.BOLD,
+                            new String(Arrays.copyOfRange(mNameBuffer.data, 0, size)),
+                            boldStart, boldEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE));
+                } else {
+                    nameView.setText(mNameBuffer.data, 0, size);
+                }
             }
         } else {
             nameView.setText(mUnknownNameText);
@@ -934,7 +965,9 @@
             }
 
             String string = new String(text.data, 0, text.sizeCopied);
-            SpannableString name = new SpannableString(string);
+            SpannableString name = new SpannableString(
+                    FormatUtils.applyStyleToSpan(Typeface.BOLD, string, 0, index,
+                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE));
             name.setSpan(mPrefixColorSpan, index, index + mHighlightedPrefix.length, 0 /* flags */);
             textView.setText(name);
         } else {
diff --git a/src/com/android/contacts/list/EmailAddressListAdapter.java b/src/com/android/contacts/list/EmailAddressListAdapter.java
index 96c69f9..eae02b5 100644
--- a/src/com/android/contacts/list/EmailAddressListAdapter.java
+++ b/src/com/android/contacts/list/EmailAddressListAdapter.java
@@ -168,8 +168,8 @@
     }
 
     protected void bindName(final ContactListItemView view, Cursor cursor) {
-        view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
-                mAlternativeDisplayNameColumnIndex);
+        view.showDisplayName(cursor, mDisplayNameColumnIndex, mAlternativeDisplayNameColumnIndex,
+                isNameHighlightingEnabled(), getContactNameDisplayOrder());
 //        view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
     }
 
diff --git a/src/com/android/contacts/list/LegacyContactListAdapter.java b/src/com/android/contacts/list/LegacyContactListAdapter.java
index 6747d1f..ffc8fc3 100644
--- a/src/com/android/contacts/list/LegacyContactListAdapter.java
+++ b/src/com/android/contacts/list/LegacyContactListAdapter.java
@@ -85,7 +85,8 @@
     }
 
     protected void bindName(final ContactListItemView view, Cursor cursor) {
-        view.showDisplayName(cursor, PERSON_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+        view.showDisplayName(cursor, PERSON_DISPLAY_NAME_COLUMN_INDEX, 0, false,
+                getContactNameDisplayOrder());
         view.showPhoneticName(cursor, PERSON_PHONETIC_NAME_COLUMN_INDEX);
     }
 
diff --git a/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java b/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
index 47747fb..547650d 100644
--- a/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
+++ b/src/com/android/contacts/list/LegacyPhoneNumberListAdapter.java
@@ -89,7 +89,8 @@
     }
 
     protected void bindName(final ContactListItemView view, Cursor cursor) {
-        view.showDisplayName(cursor, PHONE_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+        view.showDisplayName(cursor, PHONE_DISPLAY_NAME_COLUMN_INDEX, 0, false,
+                getContactNameDisplayOrder());
         view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
     }
 
diff --git a/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java b/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
index 3796c62..48b3f0c 100644
--- a/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
+++ b/src/com/android/contacts/list/LegacyPostalAddressListAdapter.java
@@ -90,7 +90,8 @@
     }
 
     protected void bindName(final ContactListItemView view, Cursor cursor) {
-        view.showDisplayName(cursor, POSTAL_DISPLAY_NAME_COLUMN_INDEX, false, 0);
+        view.showDisplayName(cursor, POSTAL_DISPLAY_NAME_COLUMN_INDEX, 0, false,
+                getContactNameDisplayOrder());
         view.showPhoneticName(cursor, POSTAL_PHONETIC_NAME_COLUMN_INDEX);
     }
 
diff --git a/src/com/android/contacts/list/PhoneNumberListAdapter.java b/src/com/android/contacts/list/PhoneNumberListAdapter.java
index 48a6042..9356bb6 100644
--- a/src/com/android/contacts/list/PhoneNumberListAdapter.java
+++ b/src/com/android/contacts/list/PhoneNumberListAdapter.java
@@ -185,8 +185,8 @@
     }
 
     protected void bindName(final ContactListItemView view, Cursor cursor) {
-        view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
-                mAlternativeDisplayNameColumnIndex);
+        view.showDisplayName(cursor, mDisplayNameColumnIndex, mAlternativeDisplayNameColumnIndex,
+                isNameHighlightingEnabled(), getContactNameDisplayOrder());
         view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
     }
 
diff --git a/src/com/android/contacts/list/PostalAddressListAdapter.java b/src/com/android/contacts/list/PostalAddressListAdapter.java
index 8b3c75d..86d465a 100644
--- a/src/com/android/contacts/list/PostalAddressListAdapter.java
+++ b/src/com/android/contacts/list/PostalAddressListAdapter.java
@@ -156,8 +156,8 @@
     }
 
     protected void bindName(final ContactListItemView view, Cursor cursor) {
-        view.showDisplayName(cursor, mDisplayNameColumnIndex, isNameHighlightingEnabled(),
-                mAlternativeDisplayNameColumnIndex);
+        view.showDisplayName(cursor, mDisplayNameColumnIndex, mAlternativeDisplayNameColumnIndex,
+                isNameHighlightingEnabled(), getContactNameDisplayOrder());
 //        view.showPhoneticName(cursor, PHONE_PHONETIC_NAME_COLUMN_INDEX);
     }
 
diff --git a/src/com/android/contacts/model/AccountType.java b/src/com/android/contacts/model/AccountType.java
index 3cce25d..462f7ad 100644
--- a/src/com/android/contacts/model/AccountType.java
+++ b/src/com/android/contacts/model/AccountType.java
@@ -249,6 +249,7 @@
         public boolean optional;
         public boolean shortForm;
         public boolean longForm;
+        public boolean isFullName;
 
         public EditField(String column, int titleRes) {
             this.column = column;
@@ -279,6 +280,11 @@
             this.minLines = minLines;
             return this;
         }
+
+        public EditField setIsFullName(boolean isFullName) {
+            this.isFullName = isFullName;
+            return this;
+        }
     }
 
     /**
diff --git a/src/com/android/contacts/model/BaseAccountType.java b/src/com/android/contacts/model/BaseAccountType.java
index ee08522..fd8f914 100644
--- a/src/com/android/contacts/model/BaseAccountType.java
+++ b/src/com/android/contacts/model/BaseAccountType.java
@@ -136,7 +136,7 @@
 
         kind.fieldList = Lists.newArrayList();
         kind.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
-                R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true));
+                R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true).setIsFullName(true));
 
         boolean displayOrderPrimary =
                 context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index 25afe4d..9d527df 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -16,12 +16,10 @@
 
 package com.android.contacts.model;
 
-import com.google.android.collect.Lists;
-
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.editor.EventFieldEditorView;
+import com.android.contacts.util.NameConverter;
 import com.android.contacts.editor.PhoneticNameEditorView;
-import com.android.contacts.editor.StructuredNameEditorView;
 import com.android.contacts.model.AccountType.EditField;
 import com.android.contacts.model.AccountType.EditType;
 import com.android.contacts.model.AccountType.EventEditType;
@@ -156,7 +154,7 @@
      */
     private static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind,
             EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) {
-        final ArrayList<EditType> validTypes = Lists.newArrayList();
+        final ArrayList<EditType> validTypes = new ArrayList<EditType>();
 
         // Bail early if no types provided
         if (!hasEditTypes(kind)) return validTypes;
@@ -1038,8 +1036,7 @@
         if (!TextUtils.isEmpty(displayName)) {
             if (!supportDisplayName) {
                 // Old data has a display name, while the new account doesn't allow it.
-                StructuredNameEditorView.buildStructuredNameFromFullName(
-                        context, displayName, values);
+                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);
@@ -1048,18 +1045,10 @@
             if (supportDisplayName) {
                 // Old data does not have display name, while the new account requires it.
                 values.put(StructuredName.DISPLAY_NAME,
-                        StructuredNameEditorView.buildFullNameFromStructuredName(context,
-                                values.getAsString(StructuredName.PREFIX),
-                                values.getAsString(StructuredName.GIVEN_NAME),
-                                values.getAsString(StructuredName.MIDDLE_NAME),
-                                values.getAsString(StructuredName.FAMILY_NAME),
-                                values.getAsString(StructuredName.SUFFIX)));
-
-                values.remove(StructuredName.PREFIX);
-                values.remove(StructuredName.GIVEN_NAME);
-                values.remove(StructuredName.MIDDLE_NAME);
-                values.remove(StructuredName.FAMILY_NAME);
-                values.remove(StructuredName.SUFFIX);
+                        NameConverter.structuredNameToDisplayName(context, values));
+                for (String field : NameConverter.STRUCTURED_NAME_FIELDS) {
+                    values.remove(field);
+                }
             }
         }
 
diff --git a/src/com/android/contacts/util/NameConverter.java b/src/com/android/contacts/util/NameConverter.java
new file mode 100644
index 0000000..9853821
--- /dev/null
+++ b/src/com/android/contacts/util/NameConverter.java
@@ -0,0 +1,166 @@
+/*
+ * 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.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 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);
+        }
+    }
+}
diff --git a/src/com/android/contacts/widget/TextHighlightingAnimation.java b/src/com/android/contacts/widget/TextHighlightingAnimation.java
index 21bbc63..882dd48 100644
--- a/src/com/android/contacts/widget/TextHighlightingAnimation.java
+++ b/src/com/android/contacts/widget/TextHighlightingAnimation.java
@@ -15,6 +15,7 @@
  */
 package com.android.contacts.widget;
 
+import com.android.contacts.format.FormatUtils;
 import com.android.internal.R;
 
 import android.database.CharArrayBuffer;
@@ -72,7 +73,7 @@
             // TODO figure out a way to avoid string allocation
             mString = new String(mText.data, 0, mText.sizeCopied);
 
-            int index = indexOf(baseText, highlightedText);
+            int index = FormatUtils.overlapPoint(baseText, highlightedText);
 
             if (index == 0 || index == -1) {
                 mDimmingEnabled = false;
@@ -83,42 +84,6 @@
             }
         }
 
-        /**
-         * An implementation of indexOf on CharArrayBuffers that finds the first match of
-         * the start of buffer2 in buffer1.  For example, indexOf("abcd", "cdef") == 2
-         */
-        private int indexOf(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
-            char[] string1 = buffer1.data;
-            char[] string2 = buffer2.data;
-            int count1 = buffer1.sizeCopied;
-            int count2 = buffer2.sizeCopied;
-
-            // Ignore matching tails of the two buffers
-            while (count1 > 0 && count2 > 0 && string1[count1 - 1] == string2[count2 - 1]) {
-                count1--;
-                count2--;
-            }
-
-            int size = count2;
-            for (int i = 0; i < count1; i++) {
-                if (i + size > count1) {
-                    size = count1 - i;
-                }
-                int j;
-                for (j = 0; j < size; j++) {
-                    if (string1[i+j] != string2[j]) {
-                        break;
-                    }
-                }
-                if (j == size) {
-                    return i;
-                }
-            }
-
-            return -1;
-        }
-
-
         @SuppressWarnings("unchecked")
         public <T> T[] getSpans(int start, int end, Class<T> type) {
             if (mDimmingEnabled) {
diff --git a/tests/src/com/android/contacts/format/FormatUtilsTests.java b/tests/src/com/android/contacts/format/FormatUtilsTests.java
new file mode 100644
index 0000000..0464adb
--- /dev/null
+++ b/tests/src/com/android/contacts/format/FormatUtilsTests.java
@@ -0,0 +1,35 @@
+/*
+ * 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.format;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+/**
+ * Test cases for format utility methods.
+ */
+@SmallTest
+public class FormatUtilsTests extends AndroidTestCase {
+
+    public void testOverlapPoint() throws Exception {
+        assertEquals(2, FormatUtils.overlapPoint("abcde", "cdefg"));
+        assertEquals(-1, FormatUtils.overlapPoint("John Doe", "John Doe"));
+        assertEquals(5, FormatUtils.overlapPoint("John Doe", "Doe, John"));
+        assertEquals(-1, FormatUtils.overlapPoint("Mr. John Doe", "Mr. Doe, John"));
+        assertEquals(13, FormatUtils.overlapPoint("John Herbert Doe", "Doe, John Herbert"));
+    }
+}
diff --git a/tests/src/com/android/contacts/util/NameConverterTests.java b/tests/src/com/android/contacts/util/NameConverterTests.java
new file mode 100644
index 0000000..e1773a7
--- /dev/null
+++ b/tests/src/com/android/contacts/util/NameConverterTests.java
@@ -0,0 +1,99 @@
+/*
+ * 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.util;
+
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tests for {@link NameConverter}.
+ */
+@SmallTest
+public class NameConverterTests extends AndroidTestCase {
+
+    public void testStructuredNameToDisplayName() {
+        Map<String, String> structuredName = new HashMap<String, String>();
+        structuredName.put(StructuredName.PREFIX, "Mr.");
+        structuredName.put(StructuredName.GIVEN_NAME, "John");
+        structuredName.put(StructuredName.MIDDLE_NAME, "Quincy");
+        structuredName.put(StructuredName.FAMILY_NAME, "Adams");
+        structuredName.put(StructuredName.SUFFIX, "Esquire");
+
+        assertEquals("Mr. John Quincy Adams, Esquire",
+                NameConverter.structuredNameToDisplayName(mContext, structuredName));
+
+        structuredName.remove(StructuredName.SUFFIX);
+        assertEquals("Mr. John Quincy Adams",
+                NameConverter.structuredNameToDisplayName(mContext, structuredName));
+
+        structuredName.remove(StructuredName.MIDDLE_NAME);
+        assertEquals("Mr. John Adams",
+                NameConverter.structuredNameToDisplayName(mContext, structuredName));
+    }
+
+    public void testDisplayNameToStructuredName() {
+        assertStructuredName("Mr. John Quincy Adams, Esquire",
+                "Mr.", "John", "Quincy", "Adams", "Esquire");
+        assertStructuredName("John Doe", null, "John", null, "Doe", null);
+        assertStructuredName("Ms. Jane Eyre", "Ms.", "Jane", null, "Eyre", null);
+        assertStructuredName("Dr Leo Spaceman, PhD", "Dr", "Leo", null, "Spaceman", "PhD");
+    }
+
+    /**
+     * Helper method to check whether a given display name parses out to the other parameters.
+     * @param displayName Display name to break into a structured name.
+     * @param prefix Expected prefix (null if not expected).
+     * @param givenName Expected given name (null if not expected).
+     * @param middleName Expected middle name (null if not expected).
+     * @param familyName Expected family name (null if not expected).
+     * @param suffix Expected suffix (null if not expected).
+     */
+    private void assertStructuredName(String displayName, String prefix,
+            String givenName, String middleName, String familyName, String suffix) {
+        Map<String, String> structuredName = NameConverter.displayNameToStructuredName(mContext,
+                displayName);
+        checkNameComponent(StructuredName.PREFIX, prefix, structuredName);
+        checkNameComponent(StructuredName.GIVEN_NAME, givenName, structuredName);
+        checkNameComponent(StructuredName.MIDDLE_NAME, middleName, structuredName);
+        checkNameComponent(StructuredName.FAMILY_NAME, familyName, structuredName);
+        checkNameComponent(StructuredName.SUFFIX, suffix, structuredName);
+        assertEquals(0, structuredName.size());
+    }
+
+    /**
+     * Checks that the given field and value are present in the structured name map (or not present
+     * if the given value is null).  If the value is present and matches, the key is removed from
+     * the map - once all components of the name are checked, the map should be empty.
+     * @param field Field to check.
+     * @param value Expected value for the field (null if it is not expected to be populated).
+     * @param structuredName The map of structured field names to values.
+     */
+    private void checkNameComponent(String field, String value,
+            Map<String, String> structuredName) {
+        if (TextUtils.isEmpty(value)) {
+            assertNull(structuredName.get(field));
+        } else {
+            assertEquals(value, structuredName.get(field));
+        }
+        structuredName.remove(field);
+    }
+}