Home | History | Annotate | Download | only in common
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.dialer.searchfragment.common;
     18 
     19 import android.content.Context;
     20 import android.support.annotation.NonNull;
     21 import android.support.v4.util.SimpleArrayMap;
     22 import android.telephony.PhoneNumberUtils;
     23 import android.text.TextUtils;
     24 import com.android.dialer.dialpadview.DialpadCharMappings;
     25 import java.util.regex.Pattern;
     26 
     27 /** Utility class for filtering, comparing and handling strings and queries. */
     28 public class QueryFilteringUtil {
     29 
     30   /**
     31    * The default character-digit map that will be used to find the digit associated with a given
     32    * character on a T9 keyboard.
     33    */
     34   private static final SimpleArrayMap<Character, Character> DEFAULT_CHAR_TO_DIGIT_MAP =
     35       DialpadCharMappings.getDefaultCharToKeyMap();
     36 
     37   /** Matches strings with "-", "(", ")", 2-9 of at least length one. */
     38   private static final Pattern T9_PATTERN = Pattern.compile("[\\-()2-9]+");
     39 
     40   /**
     41    * Returns true if the query is of T9 format and the name's T9 representation belongs to the query
     42    *
     43    * <p>Examples:
     44    *
     45    * <ul>
     46    *   <li>#nameMatchesT9Query("7", "John Smith") returns true, 7 -> 'S'
     47    *   <li>#nameMatchesT9Query("55", "Jessica Jones") returns true, 55 -> 'JJ'
     48    *   <li>#nameMatchesT9Query("56", "Jessica Jones") returns true, 56 -> 'Jo'
     49    *   <li>#nameMatchesT9Query("7", "Jessica Jones") returns false, no names start with P,Q,R or S
     50    * </ul>
     51    *
     52    * <p>When the 1st language preference uses a non-Latin alphabet (e.g., Russian) and the character
     53    * mappings for the alphabet is defined in {@link DialpadCharMappings}, the Latin alphabet will be
     54    * used first to check if the name matches the query. If they don't match, the non-Latin alphabet
     55    * will be used.
     56    *
     57    * <p>Examples (when the 1st language preference is Russian):
     58    *
     59    * <ul>
     60    *   <li>#nameMatchesT9Query("7", "John Smith") returns true, 7 -> 'S'
     61    *   <li>#nameMatchesT9Query("7", " ") returns true, 7 -> ''
     62    *   <li>#nameMatchesT9Query("77", "Pavel ") returns true, 7 -> 'P' (in the Latin alphabet),
     63    *       7 -> '' (in the Russian alphabet)
     64    * </ul>
     65    */
     66   public static boolean nameMatchesT9Query(String query, String name, Context context) {
     67     if (!T9_PATTERN.matcher(query).matches()) {
     68       return false;
     69     }
     70 
     71     query = digitsOnly(query);
     72     if (getIndexOfT9Substring(query, name, context) != -1) {
     73       return true;
     74     }
     75 
     76     // Check matches initials
     77     // TODO(calderwoodra) investigate faster implementation
     78     int queryIndex = 0;
     79 
     80     String[] names = name.toLowerCase().split("\\s");
     81     for (int i = 0; i < names.length && queryIndex < query.length(); i++) {
     82       if (TextUtils.isEmpty(names[i])) {
     83         continue;
     84       }
     85 
     86       if (getDigit(names[i].charAt(0), context) == query.charAt(queryIndex)) {
     87         queryIndex++;
     88       }
     89     }
     90 
     91     return queryIndex == query.length();
     92   }
     93 
     94   /**
     95    * Returns the index where query is contained in the T9 representation of the name.
     96    *
     97    * <p>Examples:
     98    *
     99    * <ul>
    100    *   <li>#getIndexOfT9Substring("76", "John Smith") returns 5, 76 -> 'Sm'
    101    *   <li>#nameMatchesT9Query("2226", "AAA Mom") returns 0, 2226 -> 'AAAM'
    102    *   <li>#nameMatchesT9Query("2", "Jessica Jones") returns -1, Neither 'Jessica' nor 'Jones' start
    103    *       with A, B or C
    104    * </ul>
    105    */
    106   public static int getIndexOfT9Substring(String query, String name, Context context) {
    107     query = digitsOnly(query);
    108     String t9Name = getT9Representation(name, context);
    109     String t9NameDigitsOnly = digitsOnly(t9Name);
    110     if (t9NameDigitsOnly.startsWith(query)) {
    111       return 0;
    112     }
    113 
    114     int nonLetterCount = 0;
    115     for (int i = 1; i < t9NameDigitsOnly.length(); i++) {
    116       char cur = t9Name.charAt(i);
    117       if (!Character.isDigit(cur)) {
    118         nonLetterCount++;
    119         continue;
    120       }
    121 
    122       // If the previous character isn't a digit and the current is, check for a match
    123       char prev = t9Name.charAt(i - 1);
    124       int offset = i - nonLetterCount;
    125       if (!Character.isDigit(prev) && t9NameDigitsOnly.startsWith(query, offset)) {
    126         return i;
    127       }
    128     }
    129     return -1;
    130   }
    131 
    132   /**
    133    * Returns true if the subparts of the name (split by white space) begin with the query.
    134    *
    135    * <p>Examples:
    136    *
    137    * <ul>
    138    *   <li>#nameContainsQuery("b", "Brandon") returns true
    139    *   <li>#nameContainsQuery("o", "Bob") returns false
    140    *   <li>#nameContainsQuery("o", "Bob Olive") returns true
    141    * </ul>
    142    */
    143   public static boolean nameContainsQuery(String query, String name) {
    144     if (TextUtils.isEmpty(name)) {
    145       return false;
    146     }
    147 
    148     return Pattern.compile("(^|\\s)" + Pattern.quote(query.toLowerCase()))
    149         .matcher(name.toLowerCase())
    150         .find();
    151   }
    152 
    153   /** @return true if the number belongs to the query. */
    154   public static boolean numberMatchesNumberQuery(String query, String number) {
    155     return PhoneNumberUtils.isGlobalPhoneNumber(query)
    156         && indexOfQueryNonDigitsIgnored(query, number) != -1;
    157   }
    158 
    159   /**
    160    * Checks if query is contained in number while ignoring all characters in both that are not
    161    * digits (i.e. {@link Character#isDigit(char)} returns false).
    162    *
    163    * @return index where query is found with all non-digits removed, -1 if it's not found.
    164    */
    165   static int indexOfQueryNonDigitsIgnored(@NonNull String query, @NonNull String number) {
    166     return digitsOnly(number).indexOf(digitsOnly(query));
    167   }
    168 
    169   /**
    170    * Replaces characters in the given string with their T9 representations.
    171    *
    172    * @param s The original string
    173    * @param context The context
    174    * @return The original string with characters replaced with T9 representations.
    175    */
    176   public static String getT9Representation(String s, Context context) {
    177     StringBuilder builder = new StringBuilder(s.length());
    178     for (char c : s.toLowerCase().toCharArray()) {
    179       builder.append(getDigit(c, context));
    180     }
    181     return builder.toString();
    182   }
    183 
    184   /** @return String s with only digits recognized by Character#isDigit() remaining */
    185   public static String digitsOnly(String s) {
    186     StringBuilder sb = new StringBuilder();
    187     for (int i = 0; i < s.length(); i++) {
    188       char c = s.charAt(i);
    189       if (Character.isDigit(c)) {
    190         sb.append(c);
    191       }
    192     }
    193     return sb.toString();
    194   }
    195 
    196   /**
    197    * Returns the digit on a T9 keyboard which is associated with the given lower case character.
    198    *
    199    * <p>The default character-key mapping will be used first to find a digit. If no digit is found,
    200    * try the mapping of the current default locale if it is defined in {@link DialpadCharMappings}.
    201    * If the second attempt fails, return the original character.
    202    */
    203   static char getDigit(char c, Context context) {
    204     Character digit = DEFAULT_CHAR_TO_DIGIT_MAP.get(c);
    205     if (digit != null) {
    206       return digit;
    207     }
    208 
    209     SimpleArrayMap<Character, Character> charToKeyMap =
    210         DialpadCharMappings.getCharToKeyMap(context);
    211     if (charToKeyMap != null) {
    212       digit = charToKeyMap.get(c);
    213       return digit != null ? digit : c;
    214     }
    215 
    216     return c;
    217   }
    218 }
    219