Extract a couple of utility classes for formatting.

The PrefixHighlighter class handles highlighting of a prefix in a text
view.

The DisplayNameFormatter handles both the bold of the first name and the
highlight needed when searching (using the PrefixHighlighter above).

Change-Id: Ibd1d3ecd6a88ce40de6dbc591af21c1e7260511e
diff --git a/src/com/android/contacts/format/DisplayNameFormatter.java b/src/com/android/contacts/format/DisplayNameFormatter.java
new file mode 100644
index 0000000..05698f8
--- /dev/null
+++ b/src/com/android/contacts/format/DisplayNameFormatter.java
@@ -0,0 +1,106 @@
+/*
+ * 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 com.android.contacts.widget.TextWithHighlighting;
+import com.android.contacts.widget.TextWithHighlightingFactory;
+
+import android.database.CharArrayBuffer;
+import android.graphics.Typeface;
+import android.provider.ContactsContract;
+import android.text.Spannable;
+import android.widget.TextView;
+
+import java.util.Arrays;
+
+/**
+ * Sets the content of the given text view, to contain the formatted display name, with a
+ * prefix if necessary.
+ */
+public final class DisplayNameFormatter {
+    private final CharArrayBuffer mNameBuffer = new CharArrayBuffer(128);
+    private final CharArrayBuffer mAlternateNameBuffer = new CharArrayBuffer(128);
+    private final PrefixHighlighter mPrefixHighlighter;
+
+    private TextWithHighlightingFactory mTextWithHighlightingFactory;
+    private TextWithHighlighting mTextWithHighlighting;
+    private CharSequence mUnknownNameText;
+
+    public DisplayNameFormatter(PrefixHighlighter prefixHighlighter) {
+        mPrefixHighlighter = prefixHighlighter;
+    }
+
+    public CharArrayBuffer getNameBuffer() {
+        return mNameBuffer;
+    }
+
+    public CharArrayBuffer getAlternateNameBuffer() {
+        return mAlternateNameBuffer;
+    }
+
+    public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) {
+        mTextWithHighlightingFactory = factory;
+    }
+
+    public void setUnknownNameText(CharSequence unknownNameText) {
+        mUnknownNameText = unknownNameText;
+    }
+
+    public void setDisplayName(TextView view, int displayOrder,
+            boolean highlightingEnabled, char[] highlightedPrefix) {
+        // Compute the point at which name and alternate name overlap (for bolding).
+        int overlapPoint = FormatUtils.overlapPoint(mNameBuffer, mAlternateNameBuffer);
+        int boldStart = 0;
+        int boldEnd = overlapPoint;
+        if (displayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
+            boldStart = overlapPoint;
+            boldEnd = mNameBuffer.sizeCopied;
+        }
+
+        int size = mNameBuffer.sizeCopied;
+        if (size != 0) {
+            if (highlightedPrefix != null) {
+                mPrefixHighlighter.setText(view, mNameBuffer, highlightedPrefix);
+            } else if (highlightingEnabled) {
+                if (mTextWithHighlighting == null) {
+                    mTextWithHighlighting =
+                            mTextWithHighlightingFactory.createTextWithHighlighting();
+                }
+                mTextWithHighlighting.setText(mNameBuffer, mAlternateNameBuffer);
+                if (overlapPoint > 0) {
+                    // Bold the first name.
+                    view.setText(FormatUtils.applyStyleToSpan(Typeface.BOLD,
+                            mTextWithHighlighting, boldStart, boldEnd,
+                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE));
+                } else {
+                    view.setText(mTextWithHighlighting);
+                }
+            } else {
+                if (overlapPoint > 0) {
+                    // Bold the first name.
+                    view.setText(FormatUtils.applyStyleToSpan(Typeface.BOLD,
+                            new String(Arrays.copyOfRange(mNameBuffer.data, 0, size)),
+                            boldStart, boldEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE));
+                } else {
+                    view.setText(mNameBuffer.data, 0, size);
+                }
+            }
+        } else {
+            view.setText(mUnknownNameText);
+        }
+    }
+}
diff --git a/src/com/android/contacts/format/FormatUtils.java b/src/com/android/contacts/format/FormatUtils.java
index ac34d6d..757e235 100644
--- a/src/com/android/contacts/format/FormatUtils.java
+++ b/src/com/android/contacts/format/FormatUtils.java
@@ -105,4 +105,64 @@
         text.setSpan(new StyleSpan(style), start, end, flags);
         return text;
     }
+
+    public static void copyToCharArrayBuffer(String text, CharArrayBuffer buffer) {
+        if (text != null) {
+            char[] data = buffer.data;
+            if (data == null || data.length < text.length()) {
+                buffer.data = text.toCharArray();
+            } else {
+                text.getChars(0, text.length(), data, 0);
+            }
+            buffer.sizeCopied = text.length();
+        } else {
+            buffer.sizeCopied = 0;
+        }
+    }
+
+    /**
+     * Finds the index of the first word that starts with the given prefix.
+     * <p>
+     * If not found, returns -1.
+     */
+    public static int indexOfWordPrefix(CharArrayBuffer buffer, char[] prefix) {
+        if (prefix == null || prefix.length == 0) {
+            return -1;
+        }
+
+        char[] string1 = buffer.data;
+        int bufferSize = buffer.sizeCopied;
+        int prefixSize = prefix.length;
+
+        int i = 0;
+        while (i < bufferSize) {
+
+            // Skip non-word characters
+            while (i < bufferSize && !Character.isLetterOrDigit(string1[i])) {
+                i++;
+            }
+
+            if (i + prefixSize > bufferSize) {
+                return -1;
+            }
+
+            // Compare the prefixes
+            int j;
+            for (j = 0; j < prefixSize; j++) {
+                if (Character.toUpperCase(string1[i+j]) != prefix[j]) {
+                    break;
+                }
+            }
+            if (j == prefixSize) {
+                return i;
+            }
+
+            // Skip this word
+            while (i < bufferSize && Character.isLetterOrDigit(string1[i])) {
+                i++;
+            }
+        }
+
+        return -1;
+    }
 }
diff --git a/src/com/android/contacts/format/PrefixHighlighter.java b/src/com/android/contacts/format/PrefixHighlighter.java
new file mode 100644
index 0000000..fce1d4b
--- /dev/null
+++ b/src/com/android/contacts/format/PrefixHighlighter.java
@@ -0,0 +1,75 @@
+/*
+ * 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.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.widget.TextView;
+
+/**
+ * Highlights the text in a text field.
+ */
+public class PrefixHighlighter {
+    private final CharArrayBuffer mBuffer = new CharArrayBuffer(128);
+    private final int mPrefixHighlightColor;
+
+    private ForegroundColorSpan mPrefixColorSpan;
+
+    public PrefixHighlighter(int prefixHighlightColor) {
+        mPrefixHighlightColor = prefixHighlightColor;
+    }
+
+    /**
+     * Sets the text on the given text view, highlighting the word that matches the given prefix.
+     *
+     * @param view the view on which to set the text
+     * @param text the string to use as the text
+     * @param prefix the prefix to look for
+     */
+    public void setText(TextView view, String text, char[] prefix) {
+        FormatUtils.copyToCharArrayBuffer(text, mBuffer);
+        setText(view, mBuffer, prefix);
+    }
+
+    /**
+     * Sets the text on the given text view, highlighting the word that matches the given prefix.
+     *
+     * @param view the view on which to set the text
+     * @param text the text to use as the text
+     * @param prefix the prefix to look for
+     */
+    public void setText(TextView view, CharArrayBuffer text, char[] prefix) {
+        int index = FormatUtils.indexOfWordPrefix(text, prefix);
+        if (index != -1) {
+            if (mPrefixColorSpan == null) {
+                mPrefixColorSpan = new ForegroundColorSpan(mPrefixHighlightColor);
+            }
+
+            String string = new String(text.data, 0, text.sizeCopied);
+            SpannableString name = new SpannableString(
+                    FormatUtils.applyStyleToSpan(Typeface.BOLD, string, 0, index,
+                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE));
+            name.setSpan(mPrefixColorSpan, index, index + prefix.length, 0 /* flags */);
+            view.setText(name);
+        } else {
+            view.setText(text.data, 0, text.sizeCopied);
+        }
+    }
+}
diff --git a/src/com/android/contacts/list/ContactListItemView.java b/src/com/android/contacts/list/ContactListItemView.java
index 022c2f6..bc81cd3 100644
--- a/src/com/android/contacts/list/ContactListItemView.java
+++ b/src/com/android/contacts/list/ContactListItemView.java
@@ -18,8 +18,8 @@
 
 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.format.DisplayNameFormatter;
+import com.android.contacts.format.PrefixHighlighter;
 import com.android.contacts.widget.TextWithHighlightingFactory;
 
 import android.content.Context;
@@ -31,14 +31,9 @@
 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;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.View;
@@ -49,8 +44,6 @@
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
 
-import java.util.Arrays;
-
 /**
  * A custom view for an item in the contact list.
  */
@@ -73,7 +66,6 @@
     private final int mGapBetweenLabelAndData;
     private final int mCallButtonPadding;
     private final int mPresenceIconMargin;
-    private final int mPrefixHightlightColor;
     private final int mHeaderTextColor;
     private final int mHeaderTextIndent;
     private final int mHeaderTextSize;
@@ -115,21 +107,18 @@
     private int mLine4Height;
 
     private OnClickListener mCallButtonClickListener;
-    private TextWithHighlightingFactory mTextWithHighlightingFactory;
-    private CharArrayBuffer mNameBuffer = new CharArrayBuffer(128);
     private CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
-    private CharArrayBuffer mHighlightedTextBuffer = new CharArrayBuffer(128);
-    private TextWithHighlighting mTextWithHighlighting;
     private CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
 
-    private CharSequence mUnknownNameText;
-
     private boolean mActivatedStateSupported;
 
-    private ForegroundColorSpan mPrefixColorSpan;
-
     private Rect mBoundsWithoutHeader = new Rect();
 
+    /** A helper used to highlight a prefix in a text field. */
+    private PrefixHighlighter mPrefixHighligher;
+    /** A helper used to format display names. */
+    private DisplayNameFormatter mDisplayNameFormatter;
+
     /**
      * Special class to allow the parent to be pressed without being pressed itself.
      * This way the line of a tab can be pressed, but the image itself is not.
@@ -184,9 +173,6 @@
                 R.styleable.ContactListItemView_list_item_presence_icon_margin, 0);
         mDefaultPhotoViewSize = a.getDimensionPixelOffset(
                 R.styleable.ContactListItemView_list_item_photo_size, 0);
-        mPrefixHightlightColor = a.getColor(
-                R.styleable.ContactListItemView_list_item_prefix_highlight_color, Color.GREEN);
-
         mHeaderTextIndent = a.getDimensionPixelOffset(
                 R.styleable.ContactListItemView_list_item_header_text_indent, 0);
         mHeaderTextColor = a.getColor(
@@ -194,6 +180,10 @@
         mHeaderTextSize = a.getDimensionPixelSize(
                 R.styleable.ContactListItemView_list_item_header_text_size, 12);
 
+        mPrefixHighligher = new PrefixHighlighter(
+                a.getColor(R.styleable.ContactListItemView_list_item_prefix_highlight_color,
+                        Color.GREEN));
+
         a.recycle();
 
         mHeaderBackgroundHeight = mHeaderBackgroundDrawable.getIntrinsicHeight();
@@ -202,6 +192,8 @@
         if (mActivatedBackgroundDrawable != null) {
             mActivatedBackgroundDrawable.setCallback(this);
         }
+
+        mDisplayNameFormatter = new DisplayNameFormatter(mPrefixHighligher);
     }
 
     /**
@@ -212,11 +204,11 @@
     }
 
     public void setTextWithHighlightingFactory(TextWithHighlightingFactory factory) {
-        mTextWithHighlightingFactory = factory;
+        mDisplayNameFormatter.setTextWithHighlightingFactory(factory);
     }
 
     public void setUnknownNameText(CharSequence unknownNameText) {
-        mUnknownNameText = unknownNameText;
+        mDisplayNameFormatter.setUnknownNameText(unknownNameText);
     }
 
     public void setQuickContactEnabled(boolean flag) {
@@ -767,8 +759,7 @@
                 mSnippetView.setVisibility(View.GONE);
             }
         } else {
-            getSnippetView();
-            setTextWithPrefixHighlighting(mSnippetView, text);
+            mPrefixHighligher.setText(getSnippetView(), text, mHighlightedPrefix);
             mSnippetView.setVisibility(VISIBLE);
         }
     }
@@ -813,51 +804,13 @@
 
     public void showDisplayName(Cursor cursor, int nameColumnIndex, int alternativeNameColumnIndex,
             boolean highlightingEnabled, int displayOrder) {
+        // Copy out the display name and alternate display name.
+        cursor.copyStringToBuffer(nameColumnIndex, mDisplayNameFormatter.getNameBuffer());
+        cursor.copyStringToBuffer(alternativeNameColumnIndex,
+                mDisplayNameFormatter.getAlternateNameBuffer());
 
-        // 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) {
-            if (mHighlightedPrefix != null) {
-                setTextWithPrefixHighlighting(nameView, mNameBuffer);
-            } else if (highlightingEnabled) {
-                if (mTextWithHighlighting == null) {
-                    mTextWithHighlighting =
-                            mTextWithHighlightingFactory.createTextWithHighlighting();
-                }
-                mTextWithHighlighting.setText(mNameBuffer, mHighlightedTextBuffer);
-                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 {
-                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);
-        }
+        mDisplayNameFormatter.setDisplayName(
+                getNameTextView(), displayOrder, highlightingEnabled, mHighlightedPrefix);
     }
 
     public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
@@ -942,85 +895,6 @@
         this.mActivatedStateSupported = flag;
     }
 
-    /**
-     * Sets text on the given text view, highlighting the word that matches
-     * the given prefix (see {@link #setHighlightedPrefix}).
-     */
-    private void setTextWithPrefixHighlighting(TextView textView, String text) {
-        mHighlightedTextBuffer.sizeCopied =
-                Math.min(text.length(), mHighlightedTextBuffer.data.length);
-        text.getChars(0, mHighlightedTextBuffer.sizeCopied, mHighlightedTextBuffer.data, 0);
-        setTextWithPrefixHighlighting(textView, mHighlightedTextBuffer);
-    }
-
-    /**
-     * Sets text on the given text view, highlighting the word that matches
-     * the given prefix (see {@link #setHighlightedPrefix}).
-     */
-    private void setTextWithPrefixHighlighting(TextView textView, CharArrayBuffer text) {
-        int index = indexOfWordPrefix(text, mHighlightedPrefix);
-        if (index != -1) {
-            if (mPrefixColorSpan == null) {
-                mPrefixColorSpan = new ForegroundColorSpan(mPrefixHightlightColor);
-            }
-
-            String string = new String(text.data, 0, text.sizeCopied);
-            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 {
-            textView.setText(text.data, 0, text.sizeCopied);
-        }
-    }
-
-    /**
-     * Finds the index of the word that starts with the given prefix.  If not found,
-     * returns -1.
-     */
-    private int indexOfWordPrefix(CharArrayBuffer buffer, char[] prefix) {
-        if (prefix == null || prefix.length == 0) {
-            return -1;
-        }
-
-        char[] string1 = buffer.data;
-        int count1 = buffer.sizeCopied;
-        int count2 = prefix.length;
-
-        int size = count2;
-        int i = 0;
-        while (i < count1) {
-
-            // Skip non-word characters
-            while (i < count1 && !Character.isLetterOrDigit(string1[i])) {
-                i++;
-            }
-
-            if (i + size > count1) {
-                return -1;
-            }
-
-            // Compare the prefixes
-            int j;
-            for (j = 0; j < size; j++) {
-                if (Character.toUpperCase(string1[i+j]) != prefix[j]) {
-                    break;
-                }
-            }
-            if (j == size) {
-                return i;
-            }
-
-            // Skip this word
-            while (i < count1 && Character.isLetterOrDigit(string1[i])) {
-                i++;
-            }
-        }
-
-        return -1;
-    }
-
     @Override
     public void requestLayout() {
         // We will assume that once measured this will not need to resize
diff --git a/tests/src/com/android/contacts/format/DisplayNameFormatterTest.java b/tests/src/com/android/contacts/format/DisplayNameFormatterTest.java
new file mode 100644
index 0000000..a7843c1
--- /dev/null
+++ b/tests/src/com/android/contacts/format/DisplayNameFormatterTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.provider.ContactsContract;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.widget.TextView;
+
+/**
+ * Unit tests for {@link DisplayNameFormatter}.
+ */
+@SmallTest
+public class DisplayNameFormatterTest extends AndroidTestCase {
+    private static final int TEST_PREFIX_HIGHLIGHT_COLOR = 0xFF0000;
+
+    private PrefixHighlighter mPrefixHighlighter;
+    /** The object under test. */
+    private DisplayNameFormatter mDisplayNameFormatter;
+    /** The view to on which the text is set. */
+    private TextView mView;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mPrefixHighlighter = new PrefixHighlighter(TEST_PREFIX_HIGHLIGHT_COLOR);
+        mDisplayNameFormatter = new DisplayNameFormatter(mPrefixHighlighter);
+        mView = new TextView(getContext());
+        // This guarantees that the text will be stored as a Spannable so that we can determine
+        // which styles have been applied to it.
+        mView.setText("", TextView.BufferType.SPANNABLE);
+    }
+
+    public void testSetDisplayName_Simple() {
+        setNames("John Doe", "Doe John");
+        setDisplayName();
+        SpannedTestUtils.checkHtmlText("<b>John </b>Doe", mView);
+        setNames("Jean Pierre Doe", "Doe Jean Pierre");
+        setDisplayName();
+        SpannedTestUtils.checkHtmlText("<b>Jean Pierre </b>Doe", mView);
+        setNames("John Doe Smith", "Doe Smith John");
+        setDisplayName();
+        SpannedTestUtils.checkHtmlText("<b>John </b>Doe Smith", mView);
+    }
+    public void testSetDisplayName_AccidentalOverlap() {
+        // This is probably not what we want, but we assume that the two names differ only in the
+        // order in which the two components are listed.
+        setNames("Johnson John", "Johnson Smith");
+        setDisplayName();
+        SpannedTestUtils.checkHtmlText("<b>Johnson </b>John", mView);
+    }
+
+    public void testSetDisplayName_Reversed() {
+        setNames("John Doe", "Doe John");
+        setDisplayNameReversed();
+        SpannedTestUtils.checkHtmlText("John <b>Doe</b>", mView);
+        setNames("Jean Pierre Doe", "Doe Jean Pierre");
+        setDisplayNameReversed();
+        SpannedTestUtils.checkHtmlText("Jean Pierre <b>Doe</b>", mView);
+        setNames("John Doe Smith", "Doe Smith John");
+        setDisplayNameReversed();
+        SpannedTestUtils.checkHtmlText("John <b>Doe Smith</b>", mView);
+    }
+
+    public void testSetDisplayName_NoOverlap() {
+        setNames("John Smith", "Doe Albert");
+        setDisplayName();
+        SpannedTestUtils.checkHtmlText("John Smith", mView);
+    }
+
+    public void testSetDisplayName_Prefix() {
+        setNames("John Doe", "Doe John");
+        setDisplayNameWithPrefix("DO");
+        SpannedTestUtils.checkHtmlText("<b>John </b><font color =\"#1ff0000\">Do</font>e", mView);
+    }
+
+    public void testSetDisplayName_Empty() {
+        setNames("", "");
+        setDisplayName();
+        SpannedTestUtils.checkHtmlText("", mView);
+    }
+
+    public void testSetDisplayName_Unknown() {
+        mDisplayNameFormatter.setUnknownNameText("unknown");
+        setNames("", "");
+        setDisplayName();
+        SpannedTestUtils.checkHtmlText("unknown", mView);
+    }
+
+    public void testSetDisplayName_Highlighting() {
+        mDisplayNameFormatter.setTextWithHighlightingFactory(new TestTextWithHighlightingFactory());
+        setNames("John Doe", "Sue Anne");
+        setDisplayNameWithHighlighting();
+        SpannedTestUtils.checkHtmlText("<i>John Doe</i> <i>Sue Anne</i>", mView);
+    }
+
+    public void testSetDisplayName_HighlightingAndBoldFirstName() {
+        mDisplayNameFormatter.setTextWithHighlightingFactory(new TestTextWithHighlightingFactory());
+        setNames("John Doe", "Doe John");
+        setDisplayNameWithHighlighting();
+        SpannedTestUtils.checkHtmlText("<i><b>John </b></i><i>Doe</i> <i>Doe John</i>", mView);
+    }
+
+    /**
+     * Sets the name and alternate name on the formatter.
+     *
+     * @param name the name to be display
+     * @param alternateName the alternate name to be displayed
+     */
+    private void setNames(String name, String alternateName) {
+        FormatUtils.copyToCharArrayBuffer(name, mDisplayNameFormatter.getNameBuffer());
+        FormatUtils.copyToCharArrayBuffer(alternateName,
+                mDisplayNameFormatter.getAlternateNameBuffer());
+    }
+
+    /**
+     * Sets the display name on the text view.
+     */
+    private void setDisplayName() {
+        mDisplayNameFormatter.setDisplayName(mView,
+                ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY, false, null);
+    }
+
+    /**
+     * Sets the display name on the text view using the reverted order.
+     */
+    private void setDisplayNameReversed() {
+        mDisplayNameFormatter.setDisplayName(mView,
+                ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE, false, null);
+    }
+
+    /**
+     * Sets the display name on the text view with prefix highlighting enabled.
+     */
+    private void setDisplayNameWithPrefix(String prefix) {
+        mDisplayNameFormatter.setDisplayName(mView,
+                ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY, false, prefix.toCharArray());
+    }
+
+    /**
+     * Sets the display name on the text view with highlighting enabled.
+     */
+    private void setDisplayNameWithHighlighting() {
+        mDisplayNameFormatter.setDisplayName(mView,
+                ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY, true, null);
+    }
+}
diff --git a/tests/src/com/android/contacts/format/FormatUtilsTests.java b/tests/src/com/android/contacts/format/FormatUtilsTests.java
index 0464adb..0c1c925 100644
--- a/tests/src/com/android/contacts/format/FormatUtilsTests.java
+++ b/tests/src/com/android/contacts/format/FormatUtilsTests.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts.format;
 
+import android.database.CharArrayBuffer;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -32,4 +33,64 @@
         assertEquals(-1, FormatUtils.overlapPoint("Mr. John Doe", "Mr. Doe, John"));
         assertEquals(13, FormatUtils.overlapPoint("John Herbert Doe", "Doe, John Herbert"));
     }
+
+    public void testCopyToCharArrayBuffer() {
+        CharArrayBuffer charArrayBuffer = new CharArrayBuffer(20);
+        checkCopyToCharArrayBuffer(charArrayBuffer, null, 0);
+        checkCopyToCharArrayBuffer(charArrayBuffer, "", 0);
+        checkCopyToCharArrayBuffer(charArrayBuffer, "test", 4);
+        // Check that it works after copying something into it.
+        checkCopyToCharArrayBuffer(charArrayBuffer, "", 0);
+        checkCopyToCharArrayBuffer(charArrayBuffer, "test", 4);
+        checkCopyToCharArrayBuffer(charArrayBuffer, null, 0);
+        // This requires a resize of the actual buffer.
+        checkCopyToCharArrayBuffer(charArrayBuffer, "test test test test test", 24);
+    }
+
+    /**
+     * Checks that copying into the char array buffer copies the values correctly.
+     */
+    private void checkCopyToCharArrayBuffer(CharArrayBuffer buffer, String value, int length) {
+        FormatUtils.copyToCharArrayBuffer(value, buffer);
+        assertEquals(length, buffer.sizeCopied);
+        for (int index = 0; index < length; ++index) {
+            assertEquals(value.charAt(index), buffer.data[index]);
+        }
+    }
+
+    public void testIndexOfWordPrefix_MatchingPrefix() {
+        checkIndexOfWordPrefix("test", "TE", 0);
+        checkIndexOfWordPrefix("Test", "TE", 0);
+        checkIndexOfWordPrefix("TEst", "TE", 0);
+        checkIndexOfWordPrefix("TEST", "TE", 0);
+        checkIndexOfWordPrefix("a test", "TE", 2);
+        checkIndexOfWordPrefix("test test", "TE", 0);
+        checkIndexOfWordPrefix("a test test", "TE", 2);
+    }
+
+    public void testIndexOfWordPrefix_NotMatchingPrefix() {
+        checkIndexOfWordPrefix("test", "TA", -1);
+        checkIndexOfWordPrefix("test type theme", "TA", -1);
+        checkIndexOfWordPrefix("atest retest pretest", "TEST", -1);
+        checkIndexOfWordPrefix("tes", "TEST", -1);
+    }
+
+    public void testIndexOfWordPrefix_LowerCase() {
+        // The prefix match only works if the prefix is un upper case.
+        checkIndexOfWordPrefix("test", "te", -1);
+    }
+
+    /**
+     * Checks that getting the index of a word prefix in the given text returns the expected index.
+     *
+     * @param text the text in which to look for the word
+     * @param wordPrefix the word prefix to look for
+     * @param expectedIndex the expected value to be returned by the function
+     */
+    private void checkIndexOfWordPrefix(String text, String wordPrefix, int expectedIndex) {
+        CharArrayBuffer buffer = new CharArrayBuffer(text.length());
+        FormatUtils.copyToCharArrayBuffer(text, buffer);
+        assertEquals(expectedIndex,
+                FormatUtils.indexOfWordPrefix(buffer, wordPrefix.toCharArray()));
+    }
 }
diff --git a/tests/src/com/android/contacts/format/PrefixHighligherTest.java b/tests/src/com/android/contacts/format/PrefixHighligherTest.java
new file mode 100644
index 0000000..9ec1c48
--- /dev/null
+++ b/tests/src/com/android/contacts/format/PrefixHighligherTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.test.AndroidTestCase;
+import android.text.Spannable;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.widget.TextView;
+
+/**
+ * Unit tests for {@link PrefixHighlighter}.
+ */
+public class PrefixHighligherTest extends AndroidTestCase {
+    private static final int TEST_PREFIX_HIGHLIGHT_COLOR = 0xFF0000;
+
+    /** The object under test. */
+    private PrefixHighlighter mPrefixHighlighter;
+    /** The view to on which the text is set. */
+    private TextView mView;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mPrefixHighlighter = new PrefixHighlighter(TEST_PREFIX_HIGHLIGHT_COLOR);
+        mView = new TextView(getContext());
+        // This guarantees that the text will be stored as a spannable so that we can determine
+        // which styles have been applied to it.
+        mView.setText("", TextView.BufferType.SPANNABLE);
+    }
+
+    public void testSetText_EmptyPrefix() {
+        mPrefixHighlighter.setText(mView, "", new char[0]);
+        checkTextAndNoSpans("");
+        mPrefixHighlighter.setText(mView, "test", new char[0]);
+        checkTextAndNoSpans("test");
+    }
+
+    public void testSetText_MatchingPrefix() {
+        mPrefixHighlighter.setText(mView, "test", "TE".toCharArray());
+        checkTextAndSpan("test", 0, 2);
+        mPrefixHighlighter.setText(mView, "Test", "TE".toCharArray());
+        checkTextAndSpan("Test", 0, 2);
+        mPrefixHighlighter.setText(mView, "TEst", "TE".toCharArray());
+        checkTextAndSpan("TEst", 0, 2);
+        mPrefixHighlighter.setText(mView, "a test", "TE".toCharArray());
+        checkTextAndSpan("a test", 2, 4);
+    }
+
+    public void testSetText_NotMatchingPrefix() {
+        mPrefixHighlighter.setText(mView, "test", "TA".toCharArray());
+        checkTextAndNoSpans("test");
+    }
+
+    public void testSetText_FirstMatch() {
+        mPrefixHighlighter.setText(mView, "a test's tests are not tests", "TE".toCharArray());
+        checkTextAndSpan("a test's tests are not tests", 2, 4);
+    }
+
+    public void testSetText_NoMatchingMiddleOfWord() {
+        mPrefixHighlighter.setText(mView, "atest", "TE".toCharArray());
+        checkTextAndNoSpans("atest");
+        mPrefixHighlighter.setText(mView, "atest otest", "TE".toCharArray());
+        checkTextAndNoSpans("atest otest");
+        mPrefixHighlighter.setText(mView, "atest test", "TE".toCharArray());
+        checkTextAndSpan("atest test", 6, 8);
+    }
+
+    public void testSetText_CharArrayBuffer() {
+        CharArrayBuffer buffer = new CharArrayBuffer(100);
+
+        FormatUtils.copyToCharArrayBuffer("test", buffer);
+        mPrefixHighlighter.setText(mView, buffer, new char[0]);
+        checkTextAndNoSpans("test");
+
+        FormatUtils.copyToCharArrayBuffer("a test", buffer);
+        mPrefixHighlighter.setText(mView, buffer, "TE".toCharArray());
+        checkTextAndSpan("a test", 2, 4);
+
+        FormatUtils.copyToCharArrayBuffer("test", buffer);
+        mPrefixHighlighter.setText(mView, buffer, "TA".toCharArray());
+        checkTextAndNoSpans("test");
+    }
+
+    /**
+     * Checks that the text view contains the given text and there is no highlighted prefix.
+     *
+     * @param expectedText the text expected to be in the view
+     */
+    private void checkTextAndNoSpans(String expectedText) {
+        checkTextAndOptionalSpan(expectedText, false, 0, 0);
+    }
+
+    /**
+     * Checks that the text view contains the given text and the prefix is highlighted at the given
+     * position.
+     *
+     * @param expectedText the text expected to be in the view
+     * @param expectedStart the expect start of the highlighted prefix
+     * @param expectedEnd the expect end of the highlighted prefix
+     */
+    private void checkTextAndSpan(String expectedText, int expectedStart, int expectedEnd) {
+        checkTextAndOptionalSpan(expectedText, true, expectedStart, expectedEnd);
+    }
+
+    /**
+     * Checks that the text view contains the given text and the prefix is highlighted if expected.
+     *
+     * @param expectedText the text expected to be in the view
+     * @param expectedHighlighted whether the prefix should be highlighted in the view
+     * @param expectedStart the expect start of the highlighted prefix
+     * @param expectedEnd the expect end of the highlighted prefix
+     */
+    private void checkTextAndOptionalSpan(String expectedText, boolean expectedHighlighted,
+            int expectedStart, int expectedEnd) {
+        // First check that the text is correct.
+        assertEquals(expectedText, mView.getText().toString());
+        // Get the spannable stored in the text view.
+        Spannable actualText = (Spannable) mView.getText();
+        // Get the style and color spans applied to the text.
+        StyleSpan[] styleSpans = actualText.getSpans(0, expectedText.length(), StyleSpan.class);
+        ForegroundColorSpan[] foregroundColorSpans =
+                actualText.getSpans(0, expectedText.length(), ForegroundColorSpan.class);
+        if (!expectedHighlighted) {
+            // There should be no bold or colored text.
+            assertEquals(0, styleSpans.length);
+            assertEquals(0, foregroundColorSpans.length);
+        } else {
+            // The text up to the found prefix is bold.
+            assertEquals(1, styleSpans.length);
+            StyleSpan boldSpan = styleSpans[0];
+            assertEquals(Typeface.BOLD, boldSpan.getStyle());
+            assertEquals(0, actualText.getSpanStart(boldSpan));
+            assertEquals(expectedStart, actualText.getSpanEnd(boldSpan));
+
+            // The prefix itself is in the highlight color.
+            assertEquals(1, foregroundColorSpans.length);
+            ForegroundColorSpan foregroundColorSpan = foregroundColorSpans[0];
+            assertEquals(TEST_PREFIX_HIGHLIGHT_COLOR, foregroundColorSpan.getForegroundColor());
+            assertEquals(expectedStart, actualText.getSpanStart(foregroundColorSpan));
+            assertEquals(expectedEnd, actualText.getSpanEnd(foregroundColorSpan));
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/format/SpannedTestUtils.java b/tests/src/com/android/contacts/format/SpannedTestUtils.java
new file mode 100644
index 0000000..625c6aa
--- /dev/null
+++ b/tests/src/com/android/contacts/format/SpannedTestUtils.java
@@ -0,0 +1,46 @@
+/*
+ * 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.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.widget.TextView;
+
+import junit.framework.Assert;
+
+/**
+ * Utility class to check the value of spanned text in text views.
+ */
+public class SpannedTestUtils {
+    /**
+     * Checks that the text contained in the text view matches the given HTML text.
+     *
+     * @param expectedHtmlText the expected text to be in the text view
+     * @param textView the text view from which to get the text
+     */
+    public static void checkHtmlText(String expectedHtmlText, TextView textView) {
+        String actualHtmlText = Html.toHtml((Spanned) textView.getText());
+        if (TextUtils.isEmpty(expectedHtmlText)) {
+            // If the text is empty, it does not add the <p></p> bits to it.
+            Assert.assertEquals("", actualHtmlText);
+        } else {
+            Assert.assertEquals("<p>" + expectedHtmlText + "</p>\n", actualHtmlText);
+        }
+    }
+
+}
diff --git a/tests/src/com/android/contacts/format/TestTextWithHighlightingFactory.java b/tests/src/com/android/contacts/format/TestTextWithHighlightingFactory.java
new file mode 100644
index 0000000..f2848d0
--- /dev/null
+++ b/tests/src/com/android/contacts/format/TestTextWithHighlightingFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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 com.android.contacts.widget.TextWithHighlighting;
+import com.android.contacts.widget.TextWithHighlightingFactory;
+
+import android.database.CharArrayBuffer;
+import android.graphics.Typeface;
+import android.text.SpannableStringBuilder;
+import android.text.style.StyleSpan;
+
+/** A factory for {@link TextWithHighlighting} that wraps its parts in italics. */
+public final class TestTextWithHighlightingFactory implements TextWithHighlightingFactory {
+    /** A {@link TextWithHighlighting} implementation that wraps its parts in italics. */
+    private final static class TestTextWithHighlighting extends SpannableStringBuilder
+            implements TextWithHighlighting {
+        @Override
+        public void setText(CharArrayBuffer baseText, CharArrayBuffer highlightedText) {
+            append(new String(baseText.data, 0, baseText.sizeCopied));
+            append(' ');
+            append(new String(highlightedText.data, 0, highlightedText.sizeCopied));
+            setSpan(new StyleSpan(Typeface.ITALIC), 0, baseText.sizeCopied, 0);
+            setSpan(new StyleSpan(Typeface.ITALIC), baseText.sizeCopied + 1,
+                    baseText.sizeCopied + 1 + highlightedText.sizeCopied, 0);
+        }
+    }
+
+    @Override
+    public TextWithHighlighting createTextWithHighlighting() {
+        return new TestTextWithHighlighting();
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/contacts/list/ContactListItemViewTest.java b/tests/src/com/android/contacts/list/ContactListItemViewTest.java
new file mode 100644
index 0000000..3a80e97
--- /dev/null
+++ b/tests/src/com/android/contacts/list/ContactListItemViewTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.list;
+
+import com.android.contacts.activities.DialtactsActivity;
+import com.android.contacts.format.SpannedTestUtils;
+import com.android.contacts.format.TestTextWithHighlightingFactory;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.ContactsContract;
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.TextView;
+
+/**
+ * Unit tests for {@link ContactListItemView}.
+ *
+ * It uses an {@link ActivityInstrumentationTestCase2} for {@link DialtactsActivity} because we need
+ * to have the style properly setup.
+ */
+public class ContactListItemViewTest extends ActivityInstrumentationTestCase2<DialtactsActivity> {
+    public ContactListItemViewTest() {
+        super(DialtactsActivity.class);
+    }
+
+    public void testShowDisplayName_Simple() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.showDisplayName(cursor, 0, 1, false,
+                ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY);
+
+        SpannedTestUtils.checkHtmlText("<b>John </b>Doe", view.getNameTextView());
+    }
+
+    public void testShowDisplayName_Unknown() {
+        Cursor cursor = createCursor("", "");
+        ContactListItemView view = createView();
+
+        view.setUnknownNameText("unknown");
+        view.showDisplayName(cursor, 0, 1, false,
+                ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY);
+
+        SpannedTestUtils.checkHtmlText("unknown", view.getNameTextView());
+    }
+
+    public void testShowDisplayName_WithPrefix() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.setHighlightedPrefix("DOE".toCharArray());
+        view.showDisplayName(cursor, 0, 1, false,
+                ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY);
+
+        SpannedTestUtils.checkHtmlText("<b>John </b><font color =\"#729a27\">Doe</font>",
+                view.getNameTextView());
+    }
+
+    public void testShowDisplayName_WithHighlight() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.setTextWithHighlightingFactory(new TestTextWithHighlightingFactory());
+        view.showDisplayName(cursor, 0, 1, true,
+                ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY);
+
+        SpannedTestUtils.checkHtmlText("<i><b>John </b></i><i>Doe</i> <i>Doe John</i>",
+                view.getNameTextView());
+    }
+
+    public void testSetSnippet_Prefix() {
+        ContactListItemView view = createView();
+        view.setHighlightedPrefix("TEST".toCharArray());
+        view.setSnippet("This is a test");
+        SpannedTestUtils.checkHtmlText("<b>This is a </b><font color =\"#729a27\">test</font>",
+                view.getSnippetView());
+    }
+
+    /** Creates the view to be tested. */
+    private ContactListItemView createView() {
+        ContactListItemView view = new ContactListItemView(getActivity(), null);
+        // Set the name view to use a Spannable to represent its content.
+        view.getNameTextView().setText("", TextView.BufferType.SPANNABLE);
+        return view;
+    }
+
+    /**
+     * Creates a cursor containing a pair of values.
+     *
+     * @param name the name to insert in the first column of the cursor
+     * @param alternateName the alternate name to insert in the second column of the cursor
+     * @return the newly created cursor
+     */
+    private Cursor createCursor(String name, String alternateName) {
+        MatrixCursor cursor = new MatrixCursor(new String[]{"Name", "AlternateName"});
+        cursor.moveToFirst();
+        cursor.addRow(new Object[]{name, alternateName});
+        return cursor;
+    }
+}