Updated T9 search bolding to include wrapping to the next word.

T9 bolding now works when the query continues past a single name and extends to
the next. For example, a query of "2226" would bold "[AAA M]om". This was
supported in the old search.

Bug: 71384038
Test: existing + new
PiperOrigin-RevId: 181211263
Change-Id: I7d7fe8be794f410e697ddbcb26c6bc10c7893e5a
diff --git a/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java b/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java
index 9ac6e7c..5fbcc97 100644
--- a/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java
+++ b/java/com/android/dialer/searchfragment/common/QueryBoldingUtil.java
@@ -24,6 +24,7 @@
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.style.StyleSpan;
+import com.android.dialer.common.LogUtil;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -39,6 +40,7 @@
    *   <li>"query" would bold "John [query] Smith"
    *   <li>"222" would bold "[AAA] Mom"
    *   <li>"222" would bold "[A]llen [A]lex [A]aron"
+   *   <li>"2226" would bold "[AAA M]om"
    * </ul>
    *
    * <p>Some examples of non-matches:
@@ -71,15 +73,18 @@
       }
     }
 
-    Pattern pattern = Pattern.compile("(^|\\s)" + Pattern.quote(query.toLowerCase()));
-    Matcher matcher = pattern.matcher(QueryFilteringUtil.getT9Representation(name, context));
-    if (matcher.find()) {
+    int indexOfT9Match = QueryFilteringUtil.getIndexOfT9Substring(query, name, context);
+    if (indexOfT9Match != -1) {
       // query matches the start of a T9 name (i.e. 75 -> "Jessica [Jo]nes")
-      int index = matcher.start();
-      // TODO(calderwoodra): investigate why this is consistently off by one.
-      index = index == 0 ? 0 : index + 1;
-      return getBoldedString(name, index, query.length());
+      int numBolded = query.length();
 
+      // Bold an extra character for each non-letter
+      for (int i = indexOfT9Match; i <= indexOfT9Match + numBolded && i < name.length(); i++) {
+        if (!Character.isLetter(name.charAt(i))) {
+          numBolded++;
+        }
+      }
+      return getBoldedString(name, indexOfT9Match, numBolded);
     } else {
       // query match the T9 initials (i.e. 222 -> "[A]l [B]ob [C]harlie")
       return getNameWithInitialsBolded(query, name, context);
@@ -148,6 +153,12 @@
   }
 
   private static SpannableString getBoldedString(String s, int index, int numBolded) {
+    if (numBolded + index > s.length()) {
+      LogUtil.e(
+          "QueryBoldingUtil#getBoldedString",
+          "number of bolded characters exceeded length of string.");
+      numBolded = s.length() - index;
+    }
     SpannableString span = new SpannableString(s);
     span.setSpan(
         new StyleSpan(Typeface.BOLD), index, index + numBolded, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
diff --git a/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java b/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java
index c4ff275..9add44c 100644
--- a/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java
+++ b/java/com/android/dialer/searchfragment/common/QueryFilteringUtil.java
@@ -69,9 +69,7 @@
     }
 
     query = digitsOnly(query);
-    Pattern pattern = Pattern.compile("(^|\\s)" + Pattern.quote(query));
-    if (pattern.matcher(getT9Representation(name, context)).find()) {
-      // query matches the start of a T9 name (i.e. 75 -> "Jessica [Jo]nes")
+    if (getIndexOfT9Substring(query, name, context) != -1) {
       return true;
     }
 
@@ -94,6 +92,44 @@
   }
 
   /**
+   * Returns the index where query is contained in the T9 representation of the name.
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   *   <li>#getIndexOfT9Substring("76", "John Smith") returns 5, 76 -> 'Sm'
+   *   <li>#nameMatchesT9Query("2226", "AAA Mom") returns 0, 2226 -> 'AAAM'
+   *   <li>#nameMatchesT9Query("2", "Jessica Jones") returns -1, Neither 'Jessica' nor 'Jones' start
+   *       with A, B or C
+   * </ul>
+   */
+  public static int getIndexOfT9Substring(String query, String name, Context context) {
+    query = digitsOnly(query);
+    String t9Name = getT9Representation(name, context);
+    String t9NameDigitsOnly = digitsOnly(t9Name);
+    if (t9NameDigitsOnly.startsWith(query)) {
+      return 0;
+    }
+
+    int nonLetterCount = 0;
+    for (int i = 1; i < t9NameDigitsOnly.length(); i++) {
+      char cur = t9Name.charAt(i);
+      if (!Character.isDigit(cur)) {
+        nonLetterCount++;
+        continue;
+      }
+
+      // If the previous character isn't a digit and the current is, check for a match
+      char prev = t9Name.charAt(i - 1);
+      int offset = i - nonLetterCount;
+      if (!Character.isDigit(prev) && t9NameDigitsOnly.startsWith(query, offset)) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
    * Returns true if the subparts of the name (split by white space) begin with the query.
    *
    * <p>Examples: