blob: 7fef8814cb127385e1417b34888bb2a31248985a [file] [log] [blame]
Eric Erfanianccca3152017-02-22 16:32:36 -08001/*
2 * Copyright (C) 2013 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
linyuhbf4bb052017-12-21 15:42:00 -080017package com.android.dialer.smartdial.util;
Eric Erfanianccca3152017-02-22 16:32:36 -080018
19import android.content.Context;
20import android.content.SharedPreferences;
21import android.preference.PreferenceManager;
22import android.support.annotation.VisibleForTesting;
23import android.telephony.TelephonyManager;
24import android.text.TextUtils;
linyuhbf4bb052017-12-21 15:42:00 -080025import com.android.dialer.smartdial.map.CompositeSmartDialMap;
Eric Erfanianccca3152017-02-22 16:32:36 -080026import java.util.ArrayList;
27import java.util.HashSet;
28import java.util.Set;
29
30/**
31 * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported
32 * prefix combinations for contact names, and also methods to find supported prefix combinations for
33 * contacts' phone numbers. Each contact name is separated into several tokens, such as first name,
34 * middle name, family name etc. Each phone number is also separated into country code, NANP area
35 * code, and local number if such separation is possible.
36 */
37public class SmartDialPrefix {
38
39 /**
40 * The number of starting and ending tokens in a contact's name considered for initials. For
41 * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed
42 * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced
43 * by their initials in contact name matching. Users can look up this contact by combinations of
44 * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such
45 * as "CF" "DF" "ACF" "ADF" etc.
46 */
47 private static final int LAST_TOKENS_FOR_INITIALS = 2;
48
49 private static final int FIRST_TOKENS_FOR_INITIALS = 2;
50
51 /** The country code of the user's sim card obtained by calling getSimCountryIso */
52 private static final String PREF_USER_SIM_COUNTRY_CODE =
53 "DialtactsActivity_user_sim_country_code";
54
55 private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null;
Eric Erfanianccca3152017-02-22 16:32:36 -080056
linyuh183cb712017-12-27 17:02:37 -080057 private static String userSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT;
Eric Erfanianccca3152017-02-22 16:32:36 -080058 /** Indicates whether user is in NANP regions. */
linyuh183cb712017-12-27 17:02:37 -080059 private static boolean userInNanpRegion = false;
Eric Erfanianccca3152017-02-22 16:32:36 -080060 /** Set of country names that use NANP code. */
linyuh183cb712017-12-27 17:02:37 -080061 private static Set<String> nanpCountries = null;
Eric Erfanianccca3152017-02-22 16:32:36 -080062 /** Set of supported country codes in front of the phone number. */
linyuh183cb712017-12-27 17:02:37 -080063 private static Set<String> countryCodes = null;
Eric Erfanianccca3152017-02-22 16:32:36 -080064
linyuh183cb712017-12-27 17:02:37 -080065 private static boolean nanpInitialized = false;
Eric Erfanianccca3152017-02-22 16:32:36 -080066
67 /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */
68 public static void initializeNanpSettings(Context context) {
69 final TelephonyManager manager =
70 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
71 if (manager != null) {
linyuh183cb712017-12-27 17:02:37 -080072 userSimCountryCode = manager.getSimCountryIso();
Eric Erfanianccca3152017-02-22 16:32:36 -080073 }
74
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070075 final SharedPreferences prefs =
76 PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
Eric Erfanianccca3152017-02-22 16:32:36 -080077
linyuh183cb712017-12-27 17:02:37 -080078 if (userSimCountryCode != null) {
Eric Erfanianccca3152017-02-22 16:32:36 -080079 /** Updates shared preferences with the latest country obtained from getSimCountryIso. */
linyuh183cb712017-12-27 17:02:37 -080080 prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, userSimCountryCode).apply();
Eric Erfanianccca3152017-02-22 16:32:36 -080081 } else {
82 /** Uses previously stored country code if loading fails. */
linyuh183cb712017-12-27 17:02:37 -080083 userSimCountryCode =
Eric Erfanianccca3152017-02-22 16:32:36 -080084 prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT);
85 }
86 /** Queries the NANP country list to find out whether user is in a NANP region. */
linyuh183cb712017-12-27 17:02:37 -080087 userInNanpRegion = isCountryNanp(userSimCountryCode);
88 nanpInitialized = true;
Eric Erfanianccca3152017-02-22 16:32:36 -080089 }
90
91 /**
92 * Parses a contact's name into a list of separated tokens.
93 *
94 * @param contactName Contact's name stored in string.
95 * @return A list of name tokens, for example separated first names, last name, etc.
96 */
linyuhab146532017-12-19 11:28:51 -080097 public static ArrayList<String> parseToIndexTokens(Context context, String contactName) {
Eric Erfanianccca3152017-02-22 16:32:36 -080098 final int length = contactName.length();
99 final ArrayList<String> result = new ArrayList<>();
100 char c;
101 final StringBuilder currentIndexToken = new StringBuilder();
102 /**
103 * Iterates through the whole name string. If the current character is a valid character, append
104 * it to the current token. If the current character is not a valid character, for example space
105 * " ", mark the current token as complete and add it to the list of tokens.
106 */
107 for (int i = 0; i < length; i++) {
linyuhab146532017-12-19 11:28:51 -0800108 c = CompositeSmartDialMap.normalizeCharacter(context, contactName.charAt(i));
109 if (CompositeSmartDialMap.isValidDialpadCharacter(context, c)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800110 /** Converts a character into the number on dialpad that represents the character. */
linyuhab146532017-12-19 11:28:51 -0800111 currentIndexToken.append(CompositeSmartDialMap.getDialpadIndex(context, c));
Eric Erfanianccca3152017-02-22 16:32:36 -0800112 } else {
113 if (currentIndexToken.length() != 0) {
114 result.add(currentIndexToken.toString());
115 }
116 currentIndexToken.delete(0, currentIndexToken.length());
117 }
118 }
119
120 /** Adds the last token in case it has not been added. */
121 if (currentIndexToken.length() != 0) {
122 result.add(currentIndexToken.toString());
123 }
124 return result;
125 }
126
127 /**
128 * Generates a list of strings that any prefix of any string in the list can be used to look up
129 * the contact's name.
130 *
131 * @param index The contact's name in string.
132 * @return A List of strings, whose prefix can be used to look up the contact.
133 */
linyuhab146532017-12-19 11:28:51 -0800134 public static ArrayList<String> generateNamePrefixes(Context context, String index) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800135 final ArrayList<String> result = new ArrayList<>();
136
137 /** Parses the name into a list of tokens. */
linyuhab146532017-12-19 11:28:51 -0800138 final ArrayList<String> indexTokens = parseToIndexTokens(context, index);
Eric Erfanianccca3152017-02-22 16:32:36 -0800139
140 if (indexTokens.size() > 0) {
141 /**
142 * Adds the full token combinations to the list. For example, a contact with name "Albert Ben
143 * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster"
144 * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only
145 * one token, and that spans multiple continuous tokens.
146 */
147 final StringBuilder fullNameToken = new StringBuilder();
148 for (int i = indexTokens.size() - 1; i >= 0; i--) {
149 fullNameToken.insert(0, indexTokens.get(i));
150 result.add(fullNameToken.toString());
151 }
152
153 /**
154 * Adds initial combinations to the list, with the number of initials restricted by {@link
155 * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact
156 * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings
157 * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers
158 * all cases of initial lookup.
159 */
160 ArrayList<String> fullNames = new ArrayList<>();
161 fullNames.add(indexTokens.get(indexTokens.size() - 1));
162 final int recursiveNameStart = result.size();
163 int recursiveNameEnd = result.size();
164 String initial = "";
165 for (int i = indexTokens.size() - 2; i >= 0; i--) {
166 if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS)
167 || (i < FIRST_TOKENS_FOR_INITIALS)) {
168 initial = indexTokens.get(i).substring(0, 1);
169
170 /** Recursively adds initial combinations to the list. */
171 for (int j = 0; j < fullNames.size(); ++j) {
172 result.add(initial + fullNames.get(j));
173 }
174 for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) {
175 result.add(initial + result.get(j));
176 }
177 recursiveNameEnd = result.size();
178 final String currentFullName = fullNames.get(fullNames.size() - 1);
179 fullNames.add(indexTokens.get(i) + currentFullName);
180 }
181 }
182 }
183
184 return result;
185 }
186
187 /**
188 * Computes a list of number strings based on tokens of a given phone number. Any prefix of any
189 * string in the list can be used to look up the phone number. The list include the full phone
190 * number, the national number if there is a country code in the phone number, and the local
191 * number if there is an area code in the phone number following the NANP format. For example, if
192 * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any
193 * prefix to either of the strings can be used to look up the phone number. If a user has a phone
194 * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and
195 * 3023029.
196 *
197 * @param number String of user's phone number.
198 * @return A list of strings where any prefix of any entry can be used to look up the number.
199 */
linyuhab146532017-12-19 11:28:51 -0800200 public static ArrayList<String> parseToNumberTokens(Context context, String number) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800201 final ArrayList<String> result = new ArrayList<>();
202 if (!TextUtils.isEmpty(number)) {
203 /** Adds the full number to the list. */
linyuhab146532017-12-19 11:28:51 -0800204 result.add(SmartDialNameMatcher.normalizeNumber(context, number));
Eric Erfanianccca3152017-02-22 16:32:36 -0800205
linyuhab146532017-12-19 11:28:51 -0800206 final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(context, number);
Eric Erfanianccca3152017-02-22 16:32:36 -0800207 if (phoneNumberTokens == null) {
208 return result;
209 }
210
211 if (phoneNumberTokens.countryCodeOffset != 0) {
212 result.add(
213 SmartDialNameMatcher.normalizeNumber(
linyuhab146532017-12-19 11:28:51 -0800214 context, number, phoneNumberTokens.countryCodeOffset));
Eric Erfanianccca3152017-02-22 16:32:36 -0800215 }
216
217 if (phoneNumberTokens.nanpCodeOffset != 0) {
218 result.add(
linyuhab146532017-12-19 11:28:51 -0800219 SmartDialNameMatcher.normalizeNumber(
220 context, number, phoneNumberTokens.nanpCodeOffset));
Eric Erfanianccca3152017-02-22 16:32:36 -0800221 }
222 }
223 return result;
224 }
225
226 /**
227 * Parses a phone number to find out whether it has country code and NANP area code.
228 *
229 * @param number Raw phone number.
230 * @return a PhoneNumberToken instance with country code, NANP code information.
231 */
linyuhab146532017-12-19 11:28:51 -0800232 public static PhoneNumberTokens parsePhoneNumber(Context context, String number) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800233 String countryCode = "";
234 int countryCodeOffset = 0;
235 int nanpNumberOffset = 0;
236
237 if (!TextUtils.isEmpty(number)) {
linyuhab146532017-12-19 11:28:51 -0800238 String normalizedNumber = SmartDialNameMatcher.normalizeNumber(context, number);
Eric Erfanianccca3152017-02-22 16:32:36 -0800239 if (number.charAt(0) == '+') {
240 /** If the number starts with '+', tries to find valid country code. */
241 for (int i = 1; i <= 1 + 3; i++) {
242 if (number.length() <= i) {
243 break;
244 }
245 countryCode = number.substring(1, i);
246 if (isValidCountryCode(countryCode)) {
247 countryCodeOffset = i;
248 break;
249 }
250 }
251 } else {
252 /**
253 * If the number does not start with '+', finds out whether it is in NANP format and has '1'
254 * preceding the number.
255 */
256 if ((normalizedNumber.length() == 11)
257 && (normalizedNumber.charAt(0) == '1')
linyuh183cb712017-12-27 17:02:37 -0800258 && (userInNanpRegion)) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800259 countryCode = "1";
260 countryCodeOffset = number.indexOf(normalizedNumber.charAt(1));
261 if (countryCodeOffset == -1) {
262 countryCodeOffset = 0;
263 }
264 }
265 }
266
267 /** If user is in NANP region, finds out whether a number is in NANP format. */
linyuh183cb712017-12-27 17:02:37 -0800268 if (userInNanpRegion) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800269 String areaCode = "";
270 if (countryCode.equals("") && normalizedNumber.length() == 10) {
271 /**
272 * if the number has no country code but fits the NANP format, extracts the NANP area
273 * code, and finds out offset of the local number.
274 */
275 areaCode = normalizedNumber.substring(0, 3);
276 } else if (countryCode.equals("1") && normalizedNumber.length() == 11) {
277 /**
278 * If the number has country code '1', finds out area code and offset of the local number.
279 */
280 areaCode = normalizedNumber.substring(1, 4);
281 }
282 if (!areaCode.equals("")) {
283 final int areaCodeIndex = number.indexOf(areaCode);
284 if (areaCodeIndex != -1) {
285 nanpNumberOffset = number.indexOf(areaCode) + 3;
286 }
287 }
288 }
289 }
290 return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset);
291 }
292
293 /** Checkes whether a country code is valid. */
294 private static boolean isValidCountryCode(String countryCode) {
linyuh183cb712017-12-27 17:02:37 -0800295 if (countryCodes == null) {
296 countryCodes = initCountryCodes();
Eric Erfanianccca3152017-02-22 16:32:36 -0800297 }
linyuh183cb712017-12-27 17:02:37 -0800298 return countryCodes.contains(countryCode);
Eric Erfanianccca3152017-02-22 16:32:36 -0800299 }
300
301 private static Set<String> initCountryCodes() {
302 final HashSet<String> result = new HashSet<String>();
303 result.add("1");
304 result.add("7");
305 result.add("20");
306 result.add("27");
307 result.add("30");
308 result.add("31");
309 result.add("32");
310 result.add("33");
311 result.add("34");
312 result.add("36");
313 result.add("39");
314 result.add("40");
315 result.add("41");
316 result.add("43");
317 result.add("44");
318 result.add("45");
319 result.add("46");
320 result.add("47");
321 result.add("48");
322 result.add("49");
323 result.add("51");
324 result.add("52");
325 result.add("53");
326 result.add("54");
327 result.add("55");
328 result.add("56");
329 result.add("57");
330 result.add("58");
331 result.add("60");
332 result.add("61");
333 result.add("62");
334 result.add("63");
335 result.add("64");
336 result.add("65");
337 result.add("66");
338 result.add("81");
339 result.add("82");
340 result.add("84");
341 result.add("86");
342 result.add("90");
343 result.add("91");
344 result.add("92");
345 result.add("93");
346 result.add("94");
347 result.add("95");
348 result.add("98");
349 result.add("211");
350 result.add("212");
351 result.add("213");
352 result.add("216");
353 result.add("218");
354 result.add("220");
355 result.add("221");
356 result.add("222");
357 result.add("223");
358 result.add("224");
359 result.add("225");
360 result.add("226");
361 result.add("227");
362 result.add("228");
363 result.add("229");
364 result.add("230");
365 result.add("231");
366 result.add("232");
367 result.add("233");
368 result.add("234");
369 result.add("235");
370 result.add("236");
371 result.add("237");
372 result.add("238");
373 result.add("239");
374 result.add("240");
375 result.add("241");
376 result.add("242");
377 result.add("243");
378 result.add("244");
379 result.add("245");
380 result.add("246");
381 result.add("247");
382 result.add("248");
383 result.add("249");
384 result.add("250");
385 result.add("251");
386 result.add("252");
387 result.add("253");
388 result.add("254");
389 result.add("255");
390 result.add("256");
391 result.add("257");
392 result.add("258");
393 result.add("260");
394 result.add("261");
395 result.add("262");
396 result.add("263");
397 result.add("264");
398 result.add("265");
399 result.add("266");
400 result.add("267");
401 result.add("268");
402 result.add("269");
403 result.add("290");
404 result.add("291");
405 result.add("297");
406 result.add("298");
407 result.add("299");
408 result.add("350");
409 result.add("351");
410 result.add("352");
411 result.add("353");
412 result.add("354");
413 result.add("355");
414 result.add("356");
415 result.add("357");
416 result.add("358");
417 result.add("359");
418 result.add("370");
419 result.add("371");
420 result.add("372");
421 result.add("373");
422 result.add("374");
423 result.add("375");
424 result.add("376");
425 result.add("377");
426 result.add("378");
427 result.add("379");
428 result.add("380");
429 result.add("381");
430 result.add("382");
431 result.add("385");
432 result.add("386");
433 result.add("387");
434 result.add("389");
435 result.add("420");
436 result.add("421");
437 result.add("423");
438 result.add("500");
439 result.add("501");
440 result.add("502");
441 result.add("503");
442 result.add("504");
443 result.add("505");
444 result.add("506");
445 result.add("507");
446 result.add("508");
447 result.add("509");
448 result.add("590");
449 result.add("591");
450 result.add("592");
451 result.add("593");
452 result.add("594");
453 result.add("595");
454 result.add("596");
455 result.add("597");
456 result.add("598");
457 result.add("599");
458 result.add("670");
459 result.add("672");
460 result.add("673");
461 result.add("674");
462 result.add("675");
463 result.add("676");
464 result.add("677");
465 result.add("678");
466 result.add("679");
467 result.add("680");
468 result.add("681");
469 result.add("682");
470 result.add("683");
471 result.add("685");
472 result.add("686");
473 result.add("687");
474 result.add("688");
475 result.add("689");
476 result.add("690");
477 result.add("691");
478 result.add("692");
479 result.add("800");
480 result.add("808");
481 result.add("850");
482 result.add("852");
483 result.add("853");
484 result.add("855");
485 result.add("856");
486 result.add("870");
487 result.add("878");
488 result.add("880");
489 result.add("881");
490 result.add("882");
491 result.add("883");
492 result.add("886");
493 result.add("888");
494 result.add("960");
495 result.add("961");
496 result.add("962");
497 result.add("963");
498 result.add("964");
499 result.add("965");
500 result.add("966");
501 result.add("967");
502 result.add("968");
503 result.add("970");
504 result.add("971");
505 result.add("972");
506 result.add("973");
507 result.add("974");
508 result.add("975");
509 result.add("976");
510 result.add("977");
511 result.add("979");
512 result.add("992");
513 result.add("993");
514 result.add("994");
515 result.add("995");
516 result.add("996");
517 result.add("998");
518 return result;
519 }
520
Eric Erfanianccca3152017-02-22 16:32:36 -0800521 /**
522 * Indicates whether the given country uses NANP numbers
523 *
524 * @param country ISO 3166 country code (case doesn't matter)
525 * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise
526 * @see <a href="https://en.wikipedia.org/wiki/North_American_Numbering_Plan">
527 * https://en.wikipedia.org/wiki/North_American_Numbering_Plan</a>
528 */
529 @VisibleForTesting
530 public static boolean isCountryNanp(String country) {
531 if (TextUtils.isEmpty(country)) {
532 return false;
533 }
linyuh183cb712017-12-27 17:02:37 -0800534 if (nanpCountries == null) {
535 nanpCountries = initNanpCountries();
Eric Erfanianccca3152017-02-22 16:32:36 -0800536 }
linyuh183cb712017-12-27 17:02:37 -0800537 return nanpCountries.contains(country.toUpperCase());
Eric Erfanianccca3152017-02-22 16:32:36 -0800538 }
539
540 private static Set<String> initNanpCountries() {
541 final HashSet<String> result = new HashSet<String>();
542 result.add("US"); // United States
543 result.add("CA"); // Canada
544 result.add("AS"); // American Samoa
545 result.add("AI"); // Anguilla
546 result.add("AG"); // Antigua and Barbuda
547 result.add("BS"); // Bahamas
548 result.add("BB"); // Barbados
549 result.add("BM"); // Bermuda
550 result.add("VG"); // British Virgin Islands
551 result.add("KY"); // Cayman Islands
552 result.add("DM"); // Dominica
553 result.add("DO"); // Dominican Republic
554 result.add("GD"); // Grenada
555 result.add("GU"); // Guam
556 result.add("JM"); // Jamaica
557 result.add("PR"); // Puerto Rico
558 result.add("MS"); // Montserrat
559 result.add("MP"); // Northern Mariana Islands
560 result.add("KN"); // Saint Kitts and Nevis
561 result.add("LC"); // Saint Lucia
562 result.add("VC"); // Saint Vincent and the Grenadines
563 result.add("TT"); // Trinidad and Tobago
564 result.add("TC"); // Turks and Caicos Islands
565 result.add("VI"); // U.S. Virgin Islands
566 return result;
567 }
568
569 /**
570 * Returns whether the user is in a region that uses Nanp format based on the sim location.
571 *
572 * @return Whether user is in Nanp region.
573 */
574 public static boolean getUserInNanpRegion() {
linyuh183cb712017-12-27 17:02:37 -0800575 return userInNanpRegion;
Eric Erfanianccca3152017-02-22 16:32:36 -0800576 }
577
578 /** Explicitly setting the user Nanp to the given boolean */
579 @VisibleForTesting
580 public static void setUserInNanpRegion(boolean userInNanpRegion) {
linyuh183cb712017-12-27 17:02:37 -0800581 SmartDialPrefix.userInNanpRegion = userInNanpRegion;
Eric Erfanianccca3152017-02-22 16:32:36 -0800582 }
583
584 /** Class to record phone number parsing information. */
585 public static class PhoneNumberTokens {
586
587 /** Country code of the phone number. */
588 final String countryCode;
589
590 /** Offset of national number after the country code. */
591 final int countryCodeOffset;
592
593 /** Offset of local number after NANP area code. */
594 final int nanpCodeOffset;
595
596 public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) {
597 this.countryCode = countryCode;
598 this.countryCodeOffset = countryCodeOffset;
599 this.nanpCodeOffset = nanpCodeOffset;
600 }
601 }
602}