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