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 */ nameMatchesT9Query(String query, String name, Context context)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 */ getIndexOfT9Substring(String query, String name, Context context)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 */ nameContainsQuery(String query, String name)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. */ numberMatchesNumberQuery(String query, String number)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 */ indexOfQueryNonDigitsIgnored(@onNull String query, @NonNull String number)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 */ getT9Representation(String s, Context context)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 */ digitsOnly(String s)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 */ getDigit(char c, Context context)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