Fixes highlighting of prefix.

Highlighting of a prefix (from a search) in contacts name was not done
correctly when the name was also set to be in bold. This commit changes
the logic to separate the two aspects and combines them in a way that
makes things easier to use.

Change-Id: Ibdf12d8478b7ab44ca0be82353c43ed9118d29cd
diff --git a/src/com/android/contacts/format/DisplayNameFormatter.java b/src/com/android/contacts/format/DisplayNameFormatter.java
index 05698f8..07577f2 100644
--- a/src/com/android/contacts/format/DisplayNameFormatter.java
+++ b/src/com/android/contacts/format/DisplayNameFormatter.java
@@ -25,8 +25,6 @@
 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.
@@ -62,6 +60,11 @@
 
     public void setDisplayName(TextView view, int displayOrder,
             boolean highlightingEnabled, char[] highlightedPrefix) {
+        view.setText(getDisplayName(displayOrder, highlightingEnabled, highlightedPrefix));
+    }
+
+    public CharSequence getDisplayName(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;
@@ -72,35 +75,29 @@
         }
 
         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);
+        if (size == 0) {
+            return mUnknownNameText;
         }
+
+        CharSequence text;
+        if (highlightingEnabled) {
+            if (mTextWithHighlighting == null) {
+                mTextWithHighlighting =
+                        mTextWithHighlightingFactory.createTextWithHighlighting();
+            }
+            mTextWithHighlighting.setText(mNameBuffer, mAlternateNameBuffer);
+            text = mTextWithHighlighting;
+        } else {
+            text = FormatUtils.charArrayBufferToString(mNameBuffer);
+        }
+        if (highlightedPrefix != null) {
+            text = mPrefixHighlighter.apply(text, highlightedPrefix);
+        }
+        if (overlapPoint > 0) {
+            // Bold the first or last name.
+            text = FormatUtils.applyStyleToSpan(Typeface.BOLD, text, boldStart, boldEnd,
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        }
+        return text;
     }
 }
diff --git a/src/com/android/contacts/format/FormatUtils.java b/src/com/android/contacts/format/FormatUtils.java
index 757e235..34a6078 100644
--- a/src/com/android/contacts/format/FormatUtils.java
+++ b/src/com/android/contacts/format/FormatUtils.java
@@ -120,45 +120,51 @@
         }
     }
 
+    /** Returns a String that represents the content of the given {@link CharArrayBuffer}. */
+    public static String charArrayBufferToString(CharArrayBuffer buffer) {
+        return new String(buffer.data, 0, buffer.sizeCopied);
+    }
+
     /**
      * Finds the index of the first word that starts with the given prefix.
      * <p>
      * If not found, returns -1.
+     *
+     * @param text the text in which to search for the prefix
+     * @param prefix the text to find, in upper case letters
      */
-    public static int indexOfWordPrefix(CharArrayBuffer buffer, char[] prefix) {
-        if (prefix == null || prefix.length == 0) {
+    public static int indexOfWordPrefix(CharSequence text, char[] prefix) {
+        int textLength = text.length();
+        int prefixLength = prefix.length;
+
+        if (prefix == null || prefixLength == 0 || textLength < prefixLength) {
             return -1;
         }
 
-        char[] string1 = buffer.data;
-        int bufferSize = buffer.sizeCopied;
-        int prefixSize = prefix.length;
-
         int i = 0;
-        while (i < bufferSize) {
-
+        while (i < textLength) {
             // Skip non-word characters
-            while (i < bufferSize && !Character.isLetterOrDigit(string1[i])) {
+            while (i < textLength && !Character.isLetterOrDigit(text.charAt(i))) {
                 i++;
             }
 
-            if (i + prefixSize > bufferSize) {
+            if (i + prefixLength > textLength) {
                 return -1;
             }
 
             // Compare the prefixes
             int j;
-            for (j = 0; j < prefixSize; j++) {
-                if (Character.toUpperCase(string1[i+j]) != prefix[j]) {
+            for (j = 0; j < prefixLength; j++) {
+                if (Character.toUpperCase(text.charAt(i + j)) != prefix[j]) {
                     break;
                 }
             }
-            if (j == prefixSize) {
+            if (j == prefixLength) {
                 return i;
             }
 
             // Skip this word
-            while (i < bufferSize && Character.isLetterOrDigit(string1[i])) {
+            while (i < textLength && Character.isLetterOrDigit(text.charAt(i))) {
                 i++;
             }
         }
diff --git a/src/com/android/contacts/format/PrefixHighlighter.java b/src/com/android/contacts/format/PrefixHighlighter.java
index fce1d4b..943dcb4 100644
--- a/src/com/android/contacts/format/PrefixHighlighter.java
+++ b/src/com/android/contacts/format/PrefixHighlighter.java
@@ -17,9 +17,7 @@
 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;
 
@@ -27,7 +25,6 @@
  * Highlights the text in a text field.
  */
 public class PrefixHighlighter {
-    private final CharArrayBuffer mBuffer = new CharArrayBuffer(128);
     private final int mPrefixHighlightColor;
 
     private ForegroundColorSpan mPrefixColorSpan;
@@ -44,8 +41,7 @@
      * @param prefix the prefix to look for
      */
     public void setText(TextView view, String text, char[] prefix) {
-        FormatUtils.copyToCharArrayBuffer(text, mBuffer);
-        setText(view, mBuffer, prefix);
+        view.setText(apply(text, prefix));
     }
 
     /**
@@ -56,20 +52,27 @@
      * @param prefix the prefix to look for
      */
     public void setText(TextView view, CharArrayBuffer text, char[] prefix) {
+        setText(view, FormatUtils.charArrayBufferToString(text), prefix);
+    }
+
+    /**
+     * Returns a CharSequence which highlights the given prefix if found in the given text.
+     *
+     * @param text the text to which to apply the highlight
+     * @param prefix the prefix to look for
+     */
+    public CharSequence apply(CharSequence 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);
+            SpannableString result = new SpannableString(text);
+            result.setSpan(mPrefixColorSpan, index, index + prefix.length, 0 /* flags */);
+            return result;
         } else {
-            view.setText(text.data, 0, text.sizeCopied);
+            return text;
         }
     }
 }
diff --git a/tests/src/com/android/contacts/format/DisplayNameFormatterTest.java b/tests/src/com/android/contacts/format/DisplayNameFormatterTest.java
index a7843c1..fb834c0 100644
--- a/tests/src/com/android/contacts/format/DisplayNameFormatterTest.java
+++ b/tests/src/com/android/contacts/format/DisplayNameFormatterTest.java
@@ -28,6 +28,10 @@
 @SmallTest
 public class DisplayNameFormatterTest extends AndroidTestCase {
     private static final int TEST_PREFIX_HIGHLIGHT_COLOR = 0xFF0000;
+    /** The HTML code used to mark the start of the highlighted part. */
+    private static final String START = "<font color =\"#1ff0000\">";
+    /** The HTML code used to mark the end of the highlighted part. */
+    private static final String END = "</font>";
 
     private PrefixHighlighter mPrefixHighlighter;
     /** The object under test. */
@@ -86,7 +90,26 @@
     public void testSetDisplayName_Prefix() {
         setNames("John Doe", "Doe John");
         setDisplayNameWithPrefix("DO");
-        SpannedTestUtils.checkHtmlText("<b>John </b><font color =\"#1ff0000\">Do</font>e", mView);
+        SpannedTestUtils.checkHtmlText("<b>John </b>" + START + "Do" + END + "e", mView);
+    }
+
+    public void testSetDisplayName_PrefixFirstName() {
+        setNames("John Doe", "Doe John");
+        setDisplayNameWithPrefix("JO");
+        SpannedTestUtils.checkHtmlText(START + "<b>Jo</b>" + END + "<b>hn </b>Doe", mView);
+    }
+
+    public void testSetDisplayName_PrefixMiddleName() {
+        setNames("John Paul Doe", "Doe John Paul");
+        setDisplayNameWithPrefix("PAU");
+        SpannedTestUtils.checkHtmlText("<b>John </b>" + START + "<b>Pau</b>" + END + "<b>l </b>Doe",
+                mView);
+    }
+
+    public void testSetDisplayName_ReversedPrefix() {
+        setNames("John Doe", "Doe John");
+        setDisplayNameReversedWithPrefix("DO");
+        SpannedTestUtils.checkHtmlText("John " + START + "<b>Do</b>" + END + "<b>e</b>", mView);
     }
 
     public void testSetDisplayName_Empty() {
@@ -153,6 +176,15 @@
     }
 
     /**
+     * Sets the display name reversed on the text view with prefix highlighting enabled.
+     */
+    private void setDisplayNameReversedWithPrefix(String prefix) {
+        mDisplayNameFormatter.setDisplayName(mView,
+                ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE, false,
+                prefix.toCharArray());
+    }
+
+    /**
      * Sets the display name on the text view with highlighting enabled.
      */
     private void setDisplayNameWithHighlighting() {
diff --git a/tests/src/com/android/contacts/format/FormatUtilsTests.java b/tests/src/com/android/contacts/format/FormatUtilsTests.java
index 0c1c925..42e2d53 100644
--- a/tests/src/com/android/contacts/format/FormatUtilsTests.java
+++ b/tests/src/com/android/contacts/format/FormatUtilsTests.java
@@ -47,6 +47,19 @@
         checkCopyToCharArrayBuffer(charArrayBuffer, "test test test test test", 24);
     }
 
+    public void testCharArrayBufferToString() {
+        checkCharArrayBufferToString("");
+        checkCharArrayBufferToString("test");
+        checkCharArrayBufferToString("test test test test test");
+    }
+
+    /** Checks that copying a string into a {@link CharArrayBuffer} and back works correctly. */
+    private void checkCharArrayBufferToString(String text) {
+        CharArrayBuffer buffer = new CharArrayBuffer(20);
+        FormatUtils.copyToCharArrayBuffer(text, buffer);
+        assertEquals(text, FormatUtils.charArrayBufferToString(buffer));
+    }
+
     /**
      * Checks that copying into the char array buffer copies the values correctly.
      */
@@ -88,9 +101,6 @@
      * @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()));
+        assertEquals(expectedIndex, FormatUtils.indexOfWordPrefix(text, wordPrefix.toCharArray()));
     }
 }
diff --git a/tests/src/com/android/contacts/format/PrefixHighligherTest.java b/tests/src/com/android/contacts/format/PrefixHighligherTest.java
index 9ec1c48..5500abf 100644
--- a/tests/src/com/android/contacts/format/PrefixHighligherTest.java
+++ b/tests/src/com/android/contacts/format/PrefixHighligherTest.java
@@ -17,11 +17,7 @@
 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;
 
 /**
@@ -29,6 +25,10 @@
  */
 public class PrefixHighligherTest extends AndroidTestCase {
     private static final int TEST_PREFIX_HIGHLIGHT_COLOR = 0xFF0000;
+    /** The HTML code used to mark the start of the highlighted part. */
+    private static final String START = "<font color =\"#1ff0000\">";
+    /** The HTML code used to mark the end of the highlighted part. */
+    private static final String END = "</font>";
 
     /** The object under test. */
     private PrefixHighlighter mPrefixHighlighter;
@@ -47,39 +47,46 @@
 
     public void testSetText_EmptyPrefix() {
         mPrefixHighlighter.setText(mView, "", new char[0]);
-        checkTextAndNoSpans("");
+        SpannedTestUtils.checkHtmlText("", mView);
+
         mPrefixHighlighter.setText(mView, "test", new char[0]);
-        checkTextAndNoSpans("test");
+        SpannedTestUtils.checkHtmlText("test", mView);
     }
 
     public void testSetText_MatchingPrefix() {
         mPrefixHighlighter.setText(mView, "test", "TE".toCharArray());
-        checkTextAndSpan("test", 0, 2);
+        SpannedTestUtils.checkHtmlText(START + "te" + END + "st", mView);
+
         mPrefixHighlighter.setText(mView, "Test", "TE".toCharArray());
-        checkTextAndSpan("Test", 0, 2);
+        SpannedTestUtils.checkHtmlText(START + "Te" + END + "st", mView);
+
         mPrefixHighlighter.setText(mView, "TEst", "TE".toCharArray());
-        checkTextAndSpan("TEst", 0, 2);
+        SpannedTestUtils.checkHtmlText(START + "TE" + END + "st", mView);
+
         mPrefixHighlighter.setText(mView, "a test", "TE".toCharArray());
-        checkTextAndSpan("a test", 2, 4);
+        SpannedTestUtils.checkHtmlText("a " + START + "te" + END + "st", mView);
     }
 
     public void testSetText_NotMatchingPrefix() {
         mPrefixHighlighter.setText(mView, "test", "TA".toCharArray());
-        checkTextAndNoSpans("test");
+        SpannedTestUtils.checkHtmlText("test", mView);
     }
 
     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);
+        SpannedTestUtils.checkHtmlText("a " +START + "te" + END + "st's tests are not tests",
+                mView);
     }
 
     public void testSetText_NoMatchingMiddleOfWord() {
         mPrefixHighlighter.setText(mView, "atest", "TE".toCharArray());
-        checkTextAndNoSpans("atest");
+        SpannedTestUtils.checkHtmlText("atest", mView);
+
         mPrefixHighlighter.setText(mView, "atest otest", "TE".toCharArray());
-        checkTextAndNoSpans("atest otest");
+        SpannedTestUtils.checkHtmlText("atest otest", mView);
+
         mPrefixHighlighter.setText(mView, "atest test", "TE".toCharArray());
-        checkTextAndSpan("atest test", 6, 8);
+        SpannedTestUtils.checkHtmlText("atest " + START + "te" + END + "st", mView);
     }
 
     public void testSetText_CharArrayBuffer() {
@@ -87,74 +94,14 @@
 
         FormatUtils.copyToCharArrayBuffer("test", buffer);
         mPrefixHighlighter.setText(mView, buffer, new char[0]);
-        checkTextAndNoSpans("test");
+        SpannedTestUtils.checkHtmlText("test", mView);
 
         FormatUtils.copyToCharArrayBuffer("a test", buffer);
         mPrefixHighlighter.setText(mView, buffer, "TE".toCharArray());
-        checkTextAndSpan("a test", 2, 4);
+        SpannedTestUtils.checkHtmlText("a " + START + "te" + END + "st", mView);
 
         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));
-        }
+        SpannedTestUtils.checkHtmlText("test", mView);
     }
 }
diff --git a/tests/src/com/android/contacts/list/ContactListItemViewTest.java b/tests/src/com/android/contacts/list/ContactListItemViewTest.java
index 81d8151..07ea814 100644
--- a/tests/src/com/android/contacts/list/ContactListItemViewTest.java
+++ b/tests/src/com/android/contacts/list/ContactListItemViewTest.java
@@ -35,6 +35,11 @@
  */
 @LargeTest
 public class ContactListItemViewTest extends ActivityInstrumentationTestCase2<DialtactsActivity> {
+    /** The HTML code used to mark the start of the highlighted part. */
+    private static final String START = "<font color =\"#729a27\">";
+    /** The HTML code used to mark the end of the highlighted part. */
+    private static final String END = "</font>";
+
     public ContactListItemViewTest() {
         super(DialtactsActivity.class);
     }
@@ -68,7 +73,19 @@
         view.showDisplayName(cursor, 0, 1, false,
                 ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY);
 
-        SpannedTestUtils.checkHtmlText("<b>John </b><font color =\"#729a27\">Doe</font>",
+        SpannedTestUtils.checkHtmlText("<b>John </b>" + START + "Doe" + END,
+                view.getNameTextView());
+    }
+
+    public void testShowDisplayName_WithPrefixReversed() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.setHighlightedPrefix("DOE".toCharArray());
+        view.showDisplayName(cursor, 0, 1, false,
+                ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE);
+
+        SpannedTestUtils.checkHtmlText("John " + START + "<b>Doe</b>" + END,
                 view.getNameTextView());
     }
 
@@ -88,7 +105,7 @@
         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>",
+        SpannedTestUtils.checkHtmlText("This is a " + START + "test" + END,
                 view.getSnippetView());
     }