1 /* 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 17 package com.android.dialer.smartdial; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.preference.PreferenceManager; 22 import android.support.annotation.VisibleForTesting; 23 import android.telephony.TelephonyManager; 24 import android.text.TextUtils; 25 import java.util.ArrayList; 26 import java.util.HashSet; 27 import java.util.Set; 28 29 /** 30 * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported 31 * prefix combinations for contact names, and also methods to find supported prefix combinations for 32 * contacts' phone numbers. Each contact name is separated into several tokens, such as first name, 33 * middle name, family name etc. Each phone number is also separated into country code, NANP area 34 * code, and local number if such separation is possible. 35 */ 36 public class SmartDialPrefix { 37 38 /** 39 * The number of starting and ending tokens in a contact's name considered for initials. For 40 * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed 41 * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced 42 * by their initials in contact name matching. Users can look up this contact by combinations of 43 * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such 44 * as "CF" "DF" "ACF" "ADF" etc. 45 */ 46 private static final int LAST_TOKENS_FOR_INITIALS = 2; 47 48 private static final int FIRST_TOKENS_FOR_INITIALS = 2; 49 50 /** The country code of the user's sim card obtained by calling getSimCountryIso */ 51 private static final String PREF_USER_SIM_COUNTRY_CODE = 52 "DialtactsActivity_user_sim_country_code"; 53 54 private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null; 55 /** Dialpad mapping. */ 56 private static final SmartDialMap mMap = new LatinSmartDialMap(); 57 58 private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT; 59 /** Indicates whether user is in NANP regions. */ 60 private static boolean sUserInNanpRegion = false; 61 /** Set of country names that use NANP code. */ 62 private static Set<String> sNanpCountries = null; 63 /** Set of supported country codes in front of the phone number. */ 64 private static Set<String> sCountryCodes = null; 65 66 private static boolean sNanpInitialized = false; 67 68 /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */ initializeNanpSettings(Context context)69 public static void initializeNanpSettings(Context context) { 70 final TelephonyManager manager = 71 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 72 if (manager != null) { 73 sUserSimCountryCode = manager.getSimCountryIso(); 74 } 75 76 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 77 78 if (sUserSimCountryCode != null) { 79 /** Updates shared preferences with the latest country obtained from getSimCountryIso. */ 80 prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply(); 81 } else { 82 /** Uses previously stored country code if loading fails. */ 83 sUserSimCountryCode = 84 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. */ 87 sUserInNanpRegion = isCountryNanp(sUserSimCountryCode); 88 sNanpInitialized = true; 89 } 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 */ parseToIndexTokens(String contactName)97 public static ArrayList<String> parseToIndexTokens(String contactName) { 98 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++) { 108 c = mMap.normalizeCharacter(contactName.charAt(i)); 109 if (mMap.isValidDialpadCharacter(c)) { 110 /** Converts a character into the number on dialpad that represents the character. */ 111 currentIndexToken.append(mMap.getDialpadIndex(c)); 112 } 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 */ generateNamePrefixes(String index)134 public static ArrayList<String> generateNamePrefixes(String index) { 135 final ArrayList<String> result = new ArrayList<>(); 136 137 /** Parses the name into a list of tokens. */ 138 final ArrayList<String> indexTokens = parseToIndexTokens(index); 139 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 */ parseToNumberTokens(String number)200 public static ArrayList<String> parseToNumberTokens(String number) { 201 final ArrayList<String> result = new ArrayList<>(); 202 if (!TextUtils.isEmpty(number)) { 203 /** Adds the full number to the list. */ 204 result.add(SmartDialNameMatcher.normalizeNumber(number, mMap)); 205 206 final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(number); 207 if (phoneNumberTokens == null) { 208 return result; 209 } 210 211 if (phoneNumberTokens.countryCodeOffset != 0) { 212 result.add( 213 SmartDialNameMatcher.normalizeNumber( 214 number, phoneNumberTokens.countryCodeOffset, mMap)); 215 } 216 217 if (phoneNumberTokens.nanpCodeOffset != 0) { 218 result.add( 219 SmartDialNameMatcher.normalizeNumber(number, phoneNumberTokens.nanpCodeOffset, mMap)); 220 } 221 } 222 return result; 223 } 224 225 /** 226 * Parses a phone number to find out whether it has country code and NANP area code. 227 * 228 * @param number Raw phone number. 229 * @return a PhoneNumberToken instance with country code, NANP code information. 230 */ parsePhoneNumber(String number)231 public static PhoneNumberTokens parsePhoneNumber(String number) { 232 String countryCode = ""; 233 int countryCodeOffset = 0; 234 int nanpNumberOffset = 0; 235 236 if (!TextUtils.isEmpty(number)) { 237 String normalizedNumber = SmartDialNameMatcher.normalizeNumber(number, mMap); 238 if (number.charAt(0) == '+') { 239 /** If the number starts with '+', tries to find valid country code. */ 240 for (int i = 1; i <= 1 + 3; i++) { 241 if (number.length() <= i) { 242 break; 243 } 244 countryCode = number.substring(1, i); 245 if (isValidCountryCode(countryCode)) { 246 countryCodeOffset = i; 247 break; 248 } 249 } 250 } else { 251 /** 252 * If the number does not start with '+', finds out whether it is in NANP format and has '1' 253 * preceding the number. 254 */ 255 if ((normalizedNumber.length() == 11) 256 && (normalizedNumber.charAt(0) == '1') 257 && (sUserInNanpRegion)) { 258 countryCode = "1"; 259 countryCodeOffset = number.indexOf(normalizedNumber.charAt(1)); 260 if (countryCodeOffset == -1) { 261 countryCodeOffset = 0; 262 } 263 } 264 } 265 266 /** If user is in NANP region, finds out whether a number is in NANP format. */ 267 if (sUserInNanpRegion) { 268 String areaCode = ""; 269 if (countryCode.equals("") && normalizedNumber.length() == 10) { 270 /** 271 * if the number has no country code but fits the NANP format, extracts the NANP area 272 * code, and finds out offset of the local number. 273 */ 274 areaCode = normalizedNumber.substring(0, 3); 275 } else if (countryCode.equals("1") && normalizedNumber.length() == 11) { 276 /** 277 * If the number has country code '1', finds out area code and offset of the local number. 278 */ 279 areaCode = normalizedNumber.substring(1, 4); 280 } 281 if (!areaCode.equals("")) { 282 final int areaCodeIndex = number.indexOf(areaCode); 283 if (areaCodeIndex != -1) { 284 nanpNumberOffset = number.indexOf(areaCode) + 3; 285 } 286 } 287 } 288 } 289 return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset); 290 } 291 292 /** Checkes whether a country code is valid. */ isValidCountryCode(String countryCode)293 private static boolean isValidCountryCode(String countryCode) { 294 if (sCountryCodes == null) { 295 sCountryCodes = initCountryCodes(); 296 } 297 return sCountryCodes.contains(countryCode); 298 } 299 initCountryCodes()300 private static Set<String> initCountryCodes() { 301 final HashSet<String> result = new HashSet<String>(); 302 result.add("1"); 303 result.add("7"); 304 result.add("20"); 305 result.add("27"); 306 result.add("30"); 307 result.add("31"); 308 result.add("32"); 309 result.add("33"); 310 result.add("34"); 311 result.add("36"); 312 result.add("39"); 313 result.add("40"); 314 result.add("41"); 315 result.add("43"); 316 result.add("44"); 317 result.add("45"); 318 result.add("46"); 319 result.add("47"); 320 result.add("48"); 321 result.add("49"); 322 result.add("51"); 323 result.add("52"); 324 result.add("53"); 325 result.add("54"); 326 result.add("55"); 327 result.add("56"); 328 result.add("57"); 329 result.add("58"); 330 result.add("60"); 331 result.add("61"); 332 result.add("62"); 333 result.add("63"); 334 result.add("64"); 335 result.add("65"); 336 result.add("66"); 337 result.add("81"); 338 result.add("82"); 339 result.add("84"); 340 result.add("86"); 341 result.add("90"); 342 result.add("91"); 343 result.add("92"); 344 result.add("93"); 345 result.add("94"); 346 result.add("95"); 347 result.add("98"); 348 result.add("211"); 349 result.add("212"); 350 result.add("213"); 351 result.add("216"); 352 result.add("218"); 353 result.add("220"); 354 result.add("221"); 355 result.add("222"); 356 result.add("223"); 357 result.add("224"); 358 result.add("225"); 359 result.add("226"); 360 result.add("227"); 361 result.add("228"); 362 result.add("229"); 363 result.add("230"); 364 result.add("231"); 365 result.add("232"); 366 result.add("233"); 367 result.add("234"); 368 result.add("235"); 369 result.add("236"); 370 result.add("237"); 371 result.add("238"); 372 result.add("239"); 373 result.add("240"); 374 result.add("241"); 375 result.add("242"); 376 result.add("243"); 377 result.add("244"); 378 result.add("245"); 379 result.add("246"); 380 result.add("247"); 381 result.add("248"); 382 result.add("249"); 383 result.add("250"); 384 result.add("251"); 385 result.add("252"); 386 result.add("253"); 387 result.add("254"); 388 result.add("255"); 389 result.add("256"); 390 result.add("257"); 391 result.add("258"); 392 result.add("260"); 393 result.add("261"); 394 result.add("262"); 395 result.add("263"); 396 result.add("264"); 397 result.add("265"); 398 result.add("266"); 399 result.add("267"); 400 result.add("268"); 401 result.add("269"); 402 result.add("290"); 403 result.add("291"); 404 result.add("297"); 405 result.add("298"); 406 result.add("299"); 407 result.add("350"); 408 result.add("351"); 409 result.add("352"); 410 result.add("353"); 411 result.add("354"); 412 result.add("355"); 413 result.add("356"); 414 result.add("357"); 415 result.add("358"); 416 result.add("359"); 417 result.add("370"); 418 result.add("371"); 419 result.add("372"); 420 result.add("373"); 421 result.add("374"); 422 result.add("375"); 423 result.add("376"); 424 result.add("377"); 425 result.add("378"); 426 result.add("379"); 427 result.add("380"); 428 result.add("381"); 429 result.add("382"); 430 result.add("385"); 431 result.add("386"); 432 result.add("387"); 433 result.add("389"); 434 result.add("420"); 435 result.add("421"); 436 result.add("423"); 437 result.add("500"); 438 result.add("501"); 439 result.add("502"); 440 result.add("503"); 441 result.add("504"); 442 result.add("505"); 443 result.add("506"); 444 result.add("507"); 445 result.add("508"); 446 result.add("509"); 447 result.add("590"); 448 result.add("591"); 449 result.add("592"); 450 result.add("593"); 451 result.add("594"); 452 result.add("595"); 453 result.add("596"); 454 result.add("597"); 455 result.add("598"); 456 result.add("599"); 457 result.add("670"); 458 result.add("672"); 459 result.add("673"); 460 result.add("674"); 461 result.add("675"); 462 result.add("676"); 463 result.add("677"); 464 result.add("678"); 465 result.add("679"); 466 result.add("680"); 467 result.add("681"); 468 result.add("682"); 469 result.add("683"); 470 result.add("685"); 471 result.add("686"); 472 result.add("687"); 473 result.add("688"); 474 result.add("689"); 475 result.add("690"); 476 result.add("691"); 477 result.add("692"); 478 result.add("800"); 479 result.add("808"); 480 result.add("850"); 481 result.add("852"); 482 result.add("853"); 483 result.add("855"); 484 result.add("856"); 485 result.add("870"); 486 result.add("878"); 487 result.add("880"); 488 result.add("881"); 489 result.add("882"); 490 result.add("883"); 491 result.add("886"); 492 result.add("888"); 493 result.add("960"); 494 result.add("961"); 495 result.add("962"); 496 result.add("963"); 497 result.add("964"); 498 result.add("965"); 499 result.add("966"); 500 result.add("967"); 501 result.add("968"); 502 result.add("970"); 503 result.add("971"); 504 result.add("972"); 505 result.add("973"); 506 result.add("974"); 507 result.add("975"); 508 result.add("976"); 509 result.add("977"); 510 result.add("979"); 511 result.add("992"); 512 result.add("993"); 513 result.add("994"); 514 result.add("995"); 515 result.add("996"); 516 result.add("998"); 517 return result; 518 } 519 getMap()520 public static SmartDialMap getMap() { 521 return mMap; 522 } 523 524 /** 525 * Indicates whether the given country uses NANP numbers 526 * 527 * @param country ISO 3166 country code (case doesn't matter) 528 * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise 529 * @see <a href="https://en.wikipedia.org/wiki/North_American_Numbering_Plan"> 530 * https://en.wikipedia.org/wiki/North_American_Numbering_Plan</a> 531 */ 532 @VisibleForTesting isCountryNanp(String country)533 public static boolean isCountryNanp(String country) { 534 if (TextUtils.isEmpty(country)) { 535 return false; 536 } 537 if (sNanpCountries == null) { 538 sNanpCountries = initNanpCountries(); 539 } 540 return sNanpCountries.contains(country.toUpperCase()); 541 } 542 initNanpCountries()543 private static Set<String> initNanpCountries() { 544 final HashSet<String> result = new HashSet<String>(); 545 result.add("US"); // United States 546 result.add("CA"); // Canada 547 result.add("AS"); // American Samoa 548 result.add("AI"); // Anguilla 549 result.add("AG"); // Antigua and Barbuda 550 result.add("BS"); // Bahamas 551 result.add("BB"); // Barbados 552 result.add("BM"); // Bermuda 553 result.add("VG"); // British Virgin Islands 554 result.add("KY"); // Cayman Islands 555 result.add("DM"); // Dominica 556 result.add("DO"); // Dominican Republic 557 result.add("GD"); // Grenada 558 result.add("GU"); // Guam 559 result.add("JM"); // Jamaica 560 result.add("PR"); // Puerto Rico 561 result.add("MS"); // Montserrat 562 result.add("MP"); // Northern Mariana Islands 563 result.add("KN"); // Saint Kitts and Nevis 564 result.add("LC"); // Saint Lucia 565 result.add("VC"); // Saint Vincent and the Grenadines 566 result.add("TT"); // Trinidad and Tobago 567 result.add("TC"); // Turks and Caicos Islands 568 result.add("VI"); // U.S. Virgin Islands 569 return result; 570 } 571 572 /** 573 * Returns whether the user is in a region that uses Nanp format based on the sim location. 574 * 575 * @return Whether user is in Nanp region. 576 */ getUserInNanpRegion()577 public static boolean getUserInNanpRegion() { 578 return sUserInNanpRegion; 579 } 580 581 /** Explicitly setting the user Nanp to the given boolean */ 582 @VisibleForTesting setUserInNanpRegion(boolean userInNanpRegion)583 public static void setUserInNanpRegion(boolean userInNanpRegion) { 584 sUserInNanpRegion = userInNanpRegion; 585 } 586 587 /** Class to record phone number parsing information. */ 588 public static class PhoneNumberTokens { 589 590 /** Country code of the phone number. */ 591 final String countryCode; 592 593 /** Offset of national number after the country code. */ 594 final int countryCodeOffset; 595 596 /** Offset of local number after NANP area code. */ 597 final int nanpCodeOffset; 598 PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset)599 public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) { 600 this.countryCode = countryCode; 601 this.countryCodeOffset = countryCodeOffset; 602 this.nanpCodeOffset = nanpCodeOffset; 603 } 604 } 605 } 606